hfs2/scriptLib.pas
2020-05-30 17:14:40 +02:00

2608 lines
69 KiB
ObjectPascal

{
Copyright (C) 2002-2012 Massimo Melina (www.rejetto.com)
This file is part of HFS ~ HTTP File Server.
HFS is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
HFS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with HFS; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
}
unit scriptLib;
interface
uses main, classesLib, iniFiles, types;
type
TmacroData = record
cd: TconnData;
tpl: Ttpl;
folder, f: Tfile;
afterTheList, archiveAvailable, hideExt, breaking: boolean;
aliases, tempVars: THashedStringList;
table: TStringDynArray;
logTS: boolean;
end;
var
defaultAlias: THashedStringList;
staticVars : THashedStringList; // these scripting variables are held for the whole run-time
eventScripts: Ttpl;
function tryApplyMacrosAndSymbols(var txt:string; var md:TmacroData; removeQuotings:boolean=true):boolean;
function macroQuote(s:string):string;
function runScript(script:string; table:TstringDynArray=NIL; tpl_:Ttpl=NIL; f:Tfile=NIL; folder:Tfile=NIL; cd:TconnData=NIL):string;
function runEventScript(event:string; table:TStringDynArray=NIL; cd:TconnData=NIL):string;
procedure resetLog();
implementation
uses windows, utilLib, trayLib, parserLib, graphics, classes, sysutils, StrUtils,
hslib, comctrls, math, controls, forms, clipbrd, MMsystem;
const
HEADER = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><style>'
+#13'dt, dd { margin:0; padding:0.2em 0.5em; white-space:pre; display:block; font-family:monospace; } dt { background:#dfd; } dd { background:#fdd; }'
+#13'</style></head><body>';
var
stopOnMacroRename: boolean; // this ugly global var is used to avoid endless recursion on a renaming rename event. this method won't work on a multithreaded system, but i opted for it because otherwise the changes would have been big.
cachedTpls: TcachedTpls;
function macrosLog(textIn, textOut:string; ts:boolean=FALSE):boolean;
var
s: string;
begin
s:='';
if ts then
s:='<hr>'+dateTimeToStr(now())+CRLF;
s:=s+#13'<dt>'+htmlEncode(textIn)+'</dt><dd>'+htmlEncode(textOut)+'</dd>';
if sizeOfFile(MACROS_LOG_FILE) = 0 then
s:=HEADER+s;
result:=appendTextFile(MACROS_LOG_FILE, s);
end; // macrosLog
procedure resetLog();
begin saveFile(MACROS_LOG_FILE, '') end;
function expandLinkedAccounts(account:Paccount):TStringDynArray;
var
i: integer;
begin
result:=NIL;
if account = NIL then exit;
i:=0;
result:=account.link;
while i < length(result) do
begin
account:=getAccount(result[i], TRUE);
inc(i);
if (account = NIL) or not account.enabled then continue;
addUniqueArray(result, account.link);
end;
end; // expandLinkedAccounts
function encodeMarkers(s:string):string;
var
i: integer;
t: string;
begin
for i:=0 to length(MARKERS)-1 do
begin
t:=MARKERS[i];
replace(t, '&#'+intToStr(charToUnicode(t[1]))+';', 1,1);
s:=replaceStr(s, MARKERS[i], t);
end;
result:=s;
end; // encodeMarkers
function noMacrosAllowed(s:string):string;
// prevent hack attempts
var
i: integer;
begin
i:=1;
enforceNUL(s);
repeat
i:=findMacroMarker(s, i);
if i = 0 then break;
replace(s, '&#'+intToStr(charToUnicode(s[i]))+';', i,i);
until false;
s:=reReplace(s,'%([-a-z0-9]+%)','&#37;$1', 'mi');
result:=s;
end; // noMacrosAllowed
function isMacroQuoted(s:string):boolean;
begin result:=ansiStartsStr(MARKER_QUOTE, s) and ansiEndsStr(MARKER_UNQUOTE, s) end;
function macroQuote(s:string):string;
var
t: string;
begin
enforceNUL(s);
if not anyMacroMarkerIn(s) then
begin
result:=s;
exit;
end;
// an UNQUOTE would invalidate our quoting, so let's encode any of it
t:=MARKER_UNQUOTE;
replace(t, '&#'+intToStr(charToUnicode(t[1]))+';', 1,1);
result:=MARKER_QUOTE+replaceStr(s, MARKER_UNQUOTE, t)+MARKER_UNQUOTE
end; // macroQuote
function macroDequote(s:string):string;
begin
result:=s;
s:=trim(s);
if isMacroQuoted(s) then
result:=copy(s, length(MARKER_QUOTE)+1, length(s)-length(MARKER_QUOTE)-length(MARKER_UNQUOTE) );
end; // macroDequote
function cbMacros(fullMacro:string; pars:Tstrings; cbData:pointer):string;
var
md: ^TmacroData;
name, p: string;
unnamedPars: integer; // this is a guessing of the number of unnamed parameters. just guessing because there's no true distinction between a parameter "value" named "key", and parameter "key=value"
procedure macroError(msg:string);
begin result:='<div class=macroerror>macro error: '+name+nonEmptyConcat('<br>',msg)+'</div>' end;
procedure deprecatedMacro(what:string=''; instead:string='');
begin mainfrm.add2log('WARNING, deprecated macro: '+first(what, name)+nonEmptyConcat(' - Use instead: ',instead), NIL, clRed) end;
function satisfied(p:pointer):boolean;
begin
result:=assigned(p);
if not result then
macroError('cannot be used here');
end;
procedure unsatisfied(b:boolean=TRUE);
begin if b then macroError('cannot be used here') end;
function parEx(idx:integer; name:string=''; doTrim:boolean=TRUE):string; overload;
var
i: integer;
begin
result:='';
if name > '' then
begin
i:=pars.IndexOfName(name);
if i >= 0 then
begin
result:=pars.valueFromIndex[i];
if doTrim then result:=trim(result);
exit;
end;
end;
if (idx < 0) // no numeric index accept
or (idx >= pars.count) // invalid index
or (name > '') and (pars.names[idx] > '') and not anycharIn(' '#13#10, pars.names[idx]) // this numerical index was already taken by a valid mnemonic name
then
raise Exception.create('invalid parameter index');
result:=pars[idx];
if doTrim then result:=trim(result);
end; // parEx
function parEx(name:string; doTrim:boolean=TRUE):string; overload;
begin result:=parEx(-1, name, doTrim) end;
function par(idx:integer; name:string=''; doTrim:boolean=TRUE):string; overload;
begin
try result:=parEx(idx, name, doTrim)
except result:='' end
end;
function par(name:string=''; doTrim:boolean=TRUE; defval:string=''):string; overload;
begin
try result:=parEx(-1, name, doTrim)
except result:=defval end
end;
function parI(idx:integer):int64; overload;
begin result:=strToInt64(par(idx)) end;
function parI(idx:integer; def:int64):int64; overload;
begin result:=strToInt64Def(par(idx), def) end;
function parI(name:string; def:int64):int64; overload;
begin result:=strToInt64Def(par(name), def) end;
function parF(idx:integer):extended; overload;
begin result:=strToFloat(par(idx)) end;
function parF(idx:integer; def:extended):extended; overload;
begin result:=strToFloatDef(par(idx), def) end;
function parF(name:string; def:extended):extended; overload;
begin result:=strToFloatDef(par(name), def) end;
// note this function works on N parameters
function parExist(names: array of string):boolean;
var
i: integer;
begin
result:=FALSE;
for i:=0 to length(names)-1 do
if pars.indexOfName(names[i]) < 0 then
exit;
result:=TRUE;
end; // parExist
procedure trueIf(condition:boolean);
begin if condition then result:='1' else result:='' end;
// this is for cases where normally we want a "clean" output. User can still detect outcome by using macro "length".
// Reason for having this instead of using in place a simple "result:=if_(cond, ' ')" is to evidence our purpose. It's not faster or cleaner, it's more semantic.
procedure spaceIf(condition:boolean);
begin if condition then result:=' ' else result:='' end;
function isFalse(s:string):boolean;
begin result:=(s='') or (strToFloatDef(s,1) = 0) end;
function isTrue(s:string):boolean; inline;
begin result:=not isFalse(s) end;
function getVarSpace(var varname:string):THashedStringList;
begin
varname:=trim(varname);
if ansiStartsStr(G_VAR_PREFIX, varname) then
begin
result:=staticVars;
delete(varname,1,length(G_VAR_PREFIX));
end
else if satisfied(md.cd) then
result:=md.cd.vars
else if satisfied(md.tempVars) then
result:=md.tempVars
else
raise Exception.create('no namespace available');
end; // getVarSpace
function getVar(varname:string):string; overload;
begin result:=getVarSpace(varname).values[varname] end;
// if par with name exists, then it's a var name, otherwise it's a constant value at specified index
function parVar(parname:string; idx:integer):string; overload;
begin
if parExist([parname]) then
result:=getVar(par(parname))
else
result:=pars[idx];
end; // parVar
function setVar(varname, value:string; space:THashedStringList=NIL):boolean;
var
o: Tobject;
i: integer;
begin
result:=FALSE;
if space = NIL then
space:=getVarSpace(varname);
if not satisfied(space) then exit;
i:=space.indexOfName(varname);
if i < 0 then
if value = '' then exit // all is good the way it is
else i:=space.add(varname+'='+value)
else
if value > '' then // in case of empty value, there's no need to assign, because we are going to delete it (after we cleared the bound object)
space.valueFromIndex[i]:=value;
assert(i >= 0, 'setVar: i<0');
// the previous hash object linked to this data is not valid anymore, and must be freed
o:=space.objects[i];
freeAndNIL(o);
if value = '' then
space.delete(i)
else
space.objects[i]:=NIL;
result:=TRUE;
end; // setVar
// we wrap pos() to switch between case sensitivity
function pos_(caseSensitive:boolean; ss, s:string; ofs:integer=1):integer;
begin
if caseSensitive then result:=posEx(ss,s,ofs)
else result:=ipos(ss,s,ofs)
end; // pos_
procedure allLogic(isAnd:boolean); // when not "isAnd", then it isOr ;-)
var
i: integer;
begin
// AND will return first FALSE value, or having none, the last TRUE value.
// OR will return last TRUE value, or having none, last value.
result:='';
for i:=0 to pars.count-1 do
begin
result:=par(i);
if isAnd xor isTrue(result) then exit;
end;
end; // allLogic
procedure substring();
var
i, j: integer;
s: string;
what2inc: integer;
caseSens: boolean;
begin
result:='';
// input what to be included in the result
s:=par('include');
try what2inc:=strToInt(s)
except // we also support the following values
if s = 'none' then what2inc:=0
else if s = 'both' then what2inc:=3
else if s = '1+2' then what2inc:=3
else what2inc:=1; // by default we include only the first delimiter
end;
caseSens:=isTrue(par('case'));
// find the delimiters
s:=macroDequote(par(2));
if pars[0] = '' then i:=1
else i:=pos_(caseSens, pars[0], s); // we don' trim this, so you can use blank-space as delimiter
if i = 0 then exit;
j:=pos_(caseSens, pars[1], s, i+1);
if j = 0 then j:=length(s)+1;
// apply what2inc
if what2inc and 1 = 0 then
inc(i, length(pars[0]));
if what2inc and 2 > 0 then
inc(j, length(pars[1]));
// end of the story
result:=macroQuote(copy(s, i, j-i));
end; // substring
procedure switch();
var
what, sep: string;
i, j: integer;
a: TStringDynArray;
begin
what:=par(0);
sep:=first(pars[1], ' '); // we don' trim this, so you can use blank-space as separator
i:=2;
while i < pars.count do
begin
if i = pars.count-1 then
begin
result:=macroDequote(par(i));
exit;
end;
a:=split(sep, par(i));
for j:=0 to length(a)-1 do
if sameText(a[j], what) then
begin
result:=macroDequote(par(i+1));
exit;
end;
inc(i, 2);
end;
result:='';
end; // switch
procedure cut();
var
from, upTo, l: integer;
s, v: string;
begin
v:=par('var');
if v = '' then
s:=par(2,'what')
else
s:=getVar(v);
l:=length(s);
from:=strToIntDef(par(0,'from'), 1);
if from < 0 then from:=l+from+1;
try upTo:=strToInt(parEx('to'))
except
upTo:=strToIntDef(par(1,'size'), 0);
if upTo = 0 then
upTo:=l
else if upTo > 0 then
upTo:=from+upTo-1
else
upTo:=l+upTo;
end;
result:=substr(s, from, upTo);
try setVar(parEx('remainder'), substr(s,1,from-1)+substr(s,upTo+1));
except end;
if v = '' then exit;
setVar(v, result);
result:='';
end; // cut
procedure minOrMax();
var
i: integer;
r, v: real;
min: boolean;
begin
min:=name='min';
r:=parF(0);
for i:=1 to pars.Count-1 do
begin
v:=parF(i);
if (v < r) and min
or (v > r) and not min then
r:=v;
end;
result:=floatToStr(r);
end; // minOrMax
procedure getUri();
var
i, ex, eq: integer;
vars: Tstrings;
s: string;
begin
if not satisfied(md.cd) then
exit;
try
result:=md.cd.conn.request.url;
if pars.count < 2 then exit;
s:=result;
result:=chop('?', s);
vars:=TstringList.create();
try
vars.delimiter:='&';
vars.quoteChar:=#0;
vars.delimitedText:=s;
if pars.count > 1 then
for i:=1 to pars.count-1 do
begin
s:=par(i);
if s = '' then continue;
eq:=pos('=', s);
if eq = 0 then
begin
if vars.indexOf(s) < 0 then
vars.add(pars[i]);
continue;
end;
ex:=vars.indexOfName(chop(eq,s));
if ex < 0 then
if s = '' then
continue // the parameter didn't exist, and we are trying to empty it
else
vars.add(par(i)) // didn't exist, put the whole
else
if s = '' then
vars.delete(ex) // exists, but we are trying to empty it
else
vars.valueFromIndex[ex]:=s; // exists, change the value
end;
if vars.count = 0 then exit;
for i:=vars.Count-1 downto 0 do
if vars[i] = '' then
vars.delete(i);
result:=result+'?'+vars.delimitedText;
finally vars.free end;
finally result:=macroQuote(result) end;
end; // getUri
procedure section(ofs:integer);
var
t: Ttpl;
s: string;
begin
if not satisfied(md.tpl) then exit;
s:=par(ofs);
if (par('file') = '') and ((s = '') or (pos('=',s) > 0)) then
begin // current template
result:='';
t:=md.tpl;
ofs:=parI('back', 0);
while ofs > 0 do
begin
dec(ofs);
t:=t.over;
if t = NIL then exit;
end;
try result:=t[p] except end;
exit;
end;
// template in other file
t:=Ttpl.create;
try
t.fullText:=loadTextFile(par(ofs, 'file'));
result:=t[p];
finally t.free end;
// templates outside hfs folder get quoted for security reasons
if anyCharIn('\/', par(ofs)) then
result:=macroQuote(result);
end; // section
function urlVar(k:string):string;
var
s: string;
begin
if not satisfied(md.cd) then exit;
s:=md.cd.urlvars.values[k];
if (s = '') and (md.cd.urlvars.indexOf(k) >= 0) then s:='1';
try
result:=noMacrosAllowed(s);
setVar(parEx('var'), result); // if no var is specified, it will break here, and result will have the value
result:='';
except end;
end; // urlVar
function maybeUrlvar(k:string):string;
begin
if (k = '') or (k[1] <> '?') then result:=k
else result:=urlvar(copy(k,2,MAXINT));
end; // maybeUrlvar
function compare(op,p1,p2:string):boolean;
var
r1,r2: double;
c: integer;
begin
try
r1:=StrToFloat(p1);
r2:=StrToFloat(p2);
c:=compare_(r1,r2)
except
c:=ansiCompareText(p1,p2);
end;
if op = '=' then result:= c=0
else if op = '>' then result:= c>0
else if op = '<' then result:= c<0
else if op = '>=' then result:= c>=0
else if op = '<=' then result:= c<=0
else if (op = '<>') or (op = '!=') then result:= c<>0
else result:=FALSE;
end; // compare
procedure infixOperators(ops:array of string);
var
i, j: integer;
s: string;
begin
if pars.count > 0 then exit;
for i:=0 to length(ops)-1 do
begin
j:=pos(ops[i], name);
if j = 0 then continue;
s:=trim(chop(j, length(ops[i]), name));
trueIf(compare(ops[i], maybeUrlvar(s), maybeUrlvar(trim(name))));
exit;
end;
end; // infixOperators
procedure call(code:string; ofs:integer=0);
var
i: integer;
begin
result:=code;
if pars.count=0 then
exit;
for i:=ofs to pars.Count-1 do
result:=replaceStr(result, format('$%d',[i-ofs+1]), pars[i]);
for i:=pars.count to pars.count+5 do
result:=replaceStr(result, format('$%d',[i-ofs+1]), '');
end; // call
procedure breadcrumbs();
var
e, d: string;
ae, ad: TstringDynArray;
i: integer;
fld: Tfile;
freeIt: boolean;
begin
freeIt:=FALSE;
if md.f = NIL then
fld:=md.folder
else
begin
fld:=md.f.parent;
if md.f.isTemp() then
begin
e:=extractFilePath(md.f.resource);
if length(e) > 3 then
e:=excludeTrailingPathDelimiter(e);
if e <> fld.resource then
begin
fld:=Tfile.createTemp(e);
fld.node:=md.f.node;
freeIt:=TRUE;
end
end;
end;
if not satisfied(fld) then exit;
e:=htmlEncode(encodeMarkers(fld.url(TRUE)));
d:=htmlEncode(encodeMarkers(fld.getFolder()+fld.name+'/'));
ae:=split('/', e);
ad:=split('/', d);
p:=macroDequote(p);
result:='';
e:='';
i:=length(ae)-1;
if ae[i] = '' then
dec(i);
for i:=parI('from',0) to i do
begin
e:=e+ae[i]+'/';
result:=result+xtpl(p, [
'%bread-url%', e,
'%bread-name%', ad[i],
'%bread-idx%', intToStr(i)
]);
end;
if freeIt then
freeAndNIL(fld);
end; // breadcrumbs
procedure inc_(v:integer=+1);
begin
result:='';
try setVar(p, intToStr(strToIntDef(getVar(p),0)+v*parI(1,1))) except end;
end; // inc_
procedure convert();
begin
if sameText(p, 'ansi') and sameText(par(1), 'utf-8') then
result:=string(ansiToUTF8(ansistring(par(2))))
else if sameText(p, 'utf-8') and sameText(par(1), 'ansi') then
result:=utf8ToAnsi(ansistring(par(2)))
end; // convert
procedure encodeuri();
var
i: integer;
cs: Tcharset;
begin
result:='';
try cs:=[#0..#255]-strToCharset(parEx('only'));
except
cs:=['a'..'z','A'..'Z','0'..'9',',','/','#','&','?',':','$','@','=','+']
-strToCharset(par('add'))+strToCharset(par('not'));
end;
for i:=1 to length(p) do
if charInSet(p[i], cs) then
result:=result+p[i]
else
result:=result+'%'+intToHex(ord(p[i]),2)
end; // encodeuri
procedure addFolder();
var
parent: Ttreenode;
f, old: Tfile;
fn, name: string;
// extract the path from "name", if any, and assign it to "parent"
function validateAndExtractParent():boolean;
var
i: integer;
parentF: Tfile;
begin
result:=TRUE;
i:=lastDelimiter('/',name);
if i = 0 then exit;
result:=FALSE;
parentf:=mainfrm.findFilebyURL(chop(i+1, 0, name), NIL, FALSE);
if parentf = NIL then exit;
parent:=parentf.node; // ok, this is where we'll add the folder
result:=TRUE;
end; // validateAndExtractParent
begin
result:='';
if not stringExists(p, ['real','virtual']) then exit;
parent:=NIL;
if assigned(md.folder) then
parent:=md.folder.node;
if p = 'virtual' then
begin
name:=par(1);
if not validateAndExtractParent() then exit;
f:=Tfile.createVirtualFolder(name);
end
else
begin
fn:=uri2diskMaybe(par(1));
if not isAbsolutePath(fn) and assigned(md.folder) then
fn:=expandFileName(md.folder.resource+'\'+fn);
if not directoryExists(fn) then exit; // the real folder must exists on disk
// is a name specified in the third parameter, or should we deduce it from the disk path?
name:=par(2);
if (name = '') or containsStr(name,'=') then
name:=extractFileName(fn);
if not validateAndExtractParent() then exit;
f:=Tfile.create(fn);
f.name:=name;
end;
if not validFilename(f.name) then
begin
f.free;
exit;
end;
old:=mainfrm.findFilebyURL(f.name, nodeToFile(parent), FALSE);
if assigned(old) then
if not old.isRoot()
and (not parExist(['overwrite']) or isTrue(par('overwrite'))) then
try old.node.delete() except end // delete existing one
else
begin
f.free;
exit;
end;
if mainfrm.addFile(f, parent, TRUE) = NIL then
f.free
else
spaceIf(TRUE)
end; // addFolder
procedure setItem();
var
f: Tfile;
act: TfileAction;
function get(prefix:string):TStringDynArray;
begin
result:=onlyExistentAccounts(split(';', parEx(prefix+FILEACTION2STR[act])));
uniqueStrings(result);
end;
procedure setAttr(a:TfileAttribute; parName:string);
begin
try
if isTrue(parEx(parname)) then
include(f.flags, a)
else
exclude(f.flags, a);
except end;
end; // setAttr
begin
result:='';
f:=mainfrm.findFileByURL(p, md.folder);
if f = NIL then exit; // doesn't exist
try f.setDynamicComment(macroDequote(parEx('comment'))) except end;
try
f.name:=parEx('name');
if assigned(f.node) then
f.node.text:=f.name;
except end;
try f.resource:=parEx('resource') except end;
try f.diffTpl:=parEx('diff template') except end;
try f.filesFilter:=parEx('files filter') except end;
try f.foldersFilter:=parEx('folders filter') except end;
// following commands make no sense on temporary items
if freeIfTemp(f) then exit;
setAttr(FA_HIDDEN, 'hide');
setAttr(FA_HIDDENTREE, 'hide tree');
setAttr(FA_DONT_LOG, 'no log');
setAttr(FA_ARCHIVABLE, 'archivable');
setAttr(FA_BROWSABLE, 'browsable');
setAttr(FA_DL_FORBIDDEN, 'download forbidden');
if f.isFolder() then
try f.dontCountAsDownloadMask:=parEx('not as download') except end
else
setAttr(FA_DONT_COUNT_AS_DL, 'not as download');
for act:=low(act) to high(act) do
begin
try f.accounts[act]:=get('') except end;
try addUniqueArray(f.accounts[act], get('add ')) except end;
try removeArray(f.accounts[act], get('remove ')) except end;
end;
VFSmodified:=TRUE;
mainfrm.filesBox.repaint();
end; // setItem
function getItemIcon(f:Tfile):string;
begin
if f = NIL then
result:=''
else if (f.icon >= 0) or (mainfrm.useSystemIconsChk.checked and f.isFile()) then
result:='/~img'+intToStr(f.getSystemIcon())
else if f.isFile() then
result:='/~img_file'
else if f.isFolder() then
if FA_UNIT in f.flags then
result:=format('/~img%d', [f.getIconForTreeview()])
else
result:='/~img_folder'
else if f.isLink() then
result:='/~img_link'
else
result:='';
end; // getItemIcon
procedure deleteItem();
var
f: Tfile;
begin
f:=mainfrm.findFileByURL(p);
spaceIf(assigned(f)); // so you can know if something really has been deleted
if f = NIL then exit; // doesn't exist
mainFrm.remove(f.node);
VFSmodified:=TRUE;
end; // deleteItem
procedure getItem();
var
f: Tfile;
act: TfileAction;
w: string;
function getAttr(name:string; a:TfileAttribute):boolean;
begin
result:= w = name;
if result then
trueIf(a in f.flags);
end; // setAttr
begin
result:='';
f:=mainfrm.findFileByURL(p, md.folder);
if f = NIL then exit; // doesn't exist
try
w:=par(1);
if w = 'exists' then
result:='1'
else if w = 'comment' then
result:=f.getDynamicComment()
else if w = 'resource' then
result:=f.resource
else if w = 'icon' then
result:=getItemIcon(f)
else if getAttr('hide', FA_HIDDEN)
or getAttr('hide tree', FA_HIDDENTREE)
or getAttr('no log', FA_DONT_LOG) then
exit
else if w = 'not as download' then
if f.isFolder() then
result:=f.dontCountAsDownloadMask
else
trueIf(FA_DONT_COUNT_AS_DL in f.flags);
for act:=low(act) to high(act) do
if compareText(w, FILEACTION2STR[act]) = 0 then
begin
result:=join(';', f.accounts[act]);
exit;
end;
finally freeIfTemp(f) end;
end; // getItem
procedure foreach();
var
i, e: integer;
s, code: string;
begin
e:=pars.count-2; // 3 parameters minimum (the check is outside)
code:=macroDequote(par(pars.count-1));
with TfastStringAppend.create do
try
for i:=1 to e do
begin
setVar(p, par(i));
s:=code;
applyMacrosAndSymbols(s, cbMacros, cbData);
append(s);
end;
result:=reset();
finally free end;
end; // foreach
procedure forLine();
var
lines: TStringList;
line, code, run: string;
i: integer;
begin
code:=macroDequote(par(pars.count-1));
with TfastStringAppend.create do
try
lines:=TStringList.create();
lines.text:= getVar(par('var'));
for line in lines do
begin
i:=pos('=',line);
if i > 0 then
begin
setVar('line-key', Copy(line, 1, i-1));
setVar('line-value', Copy(line, i+1, MAXINT));
end;
setVar('line', line);
run:=code;
applyMacrosAndSymbols(run, cbMacros, cbData);
append(run);
end;
result:=reset();
finally
Free;
lines.Free;
end;
end; //forLine
procedure for_();
var
b, e, i, d: integer;
s, code: string;
begin
try
b:=strToInt(par(1));
e:=strToInt(par(2));
try
d:=strToInt(par(3));
code:=par(4);
except
d:=1;
code:=par(3);
end;
if d = 0 then exit;
if (e < b) and (d > 0) then d:=-d; // we care
code:=macroDequote(code);
with TfastStringAppend.create do
try
for i:=1 to (e-b) div d+1 do
begin
setVar(p, intToStr(b));
s:=code;
applyMacrosAndSymbols(s, cbMacros, cbData);
append(s);
inc(b, d);
end;
result:=reset();
finally free end;
except end;
end; // for_
procedure while_();
var
bTest, bDo, s: string;
never: boolean;
res: TfastStringAppend;
space: THashedStringList;
start, timeout: Tdatetime;
begin
result:='';
res:=TfastStringAppend.create;
try
never:=TRUE;
bDo:=macroDequote(par(1)); // do-block
bTest:='';
// test-block
space:=NIL;
if anyMacroMarkerIn(p) then
bTest:=macroDequote(p)
else
try // lets see if the test-block is just the name of a variable
space:=getVarSpace(p);
bTest:=p;
except end;
if bTest = '' then exit;
timeout:=parF('timeout', 1)/SECONDS; // stay safe: 1 second timeout by default
start:=now();
repeat
if (timeout > 0) and (now()-start > timeout) then break;
if assigned(space) then
s:=space.values[bTest]
else
begin
s:=bTest;
applyMacrosAndSymbols(s, cbMacros, cbData);
end;
if isFalse(trim(s)) then break;
s:=bDo;
applyMacrosAndSymbols(s, cbMacros, cbData);
res.append(s);
never:=FALSE;
until false;
if never then
res.append(macroDequote(par('else'))); // else-block
finally
result:=res.reset();
try
setVar(parEx('var'), result);
result:='';
except end;
res.free();
end;
end; // while_
procedure setEncodedTable(varname, txt:string);
var
space, h: ThashedStringList;
i: integer;
begin
chopLine(txt); // first line is just a useless header
// search the variable in the varspace
space:=getVarSpace(varname);
if not satisfied(space) then exit;
i:=space.indexOfName(varname);
// eventually destroy previous object
if i >= 0 then
begin
h:=space.objects[i] as ThashedStringList;
freeAndNIL(h);
space.objects[i]:=NIL;
end;
// create the table object
h:=ThashedStringList.create();
while txt > '' do
h.add(unescapeNL(chopline(txt)));
// assign the variable value
txt:=h.text;
if i < 0 then
i:=space.add(varname+'='+txt)
else
space.valueFromIndex[i]:=txt;
// link the object
space.objects[i]:=h;
end; // setEncodedTable
procedure load(fn:string; varname:string='');
var
from, size: int64;
s: ansistring;
begin
result:='';
from:=parI('from', 0);
// 'size' has priority over 'to'
size:=parI('size', -1);
if size = -1 then
begin
size:=parI('to', -1);
if size >= 0 then
inc(size, 1-from);
end;
if size = 0 then exit;
from:=max(0,from);
if reMatch(fn, '^https?://', 'i!') > 0 then
try result:=httpGet(fn, from, size)
except result:='' end
else
begin
s:=loadFile(uri2diskMaybe(fn), from, size);
result:=UTF8ToString(s);
if result = '' then
result:=s;
end;
if varname = '' then
begin
if anyCharIn('/\',fn) then
result:=macroQuote(result);
exit;
end;
if ansiStartsStr(ENCODED_TABLE_HEADER, result) then
setEncodedTable(varname, result)
else
setVar(varname, result);
result:='';
end; // load
function uri2diskMaybeFolder(s:string):string; // like uri2diskMaybe, but limited to the path, excluding the filename
var
path: string;
begin
if ansiContainsStr(s, '/') then
begin
path:=uri2disk(chop(lastDelimiter('/\',s)+1, 0, s), md.folder);
if path > '' then
s:=path+'\'+trim(s); // mod by mars
end;
result:=s;
end; // uri2diskMaybeFolder
procedure save();
var
space, h: THashedStringList;
s: string;
i: integer;
encode: boolean;
begin
result:='';
if not parExist(['var']) then // will we work with a variable?
s:=pars[1]
else
begin
s:=par('var');
space:=getVarSpace(s);
if not satisfied(space) then exit;
i:=space.indexOfName(s);
if i < 0 then exit; // this var doesn't exit. don't write.
encode:=FALSE;
// if this is used as table, and has newlines, we must encode it to preserve associations
h:=space.objects[i] as THashedStringList;
if assigned(h) then
for i:=0 to h.count-1 do
if anyCharIn([#13,#10], h.strings[i]) then
begin
encode:=TRUE;
break;
end;
if not encode then
s:=space.valueFromIndex[i]
else
with TfastStringAppend.create do
try // table must be codified, or they won't work at load-time
append(ENCODED_TABLE_HEADER);
for i:=0 to h.count-1 do
append(escapeNL(h.strings[i])+CRLF);
s:=get();
finally free end;
end;
// now we have in 's' the content to be saved
spaceIf(saveTextFile(uri2diskMaybeFolder(p), s, name='append'));
end; // save
procedure replace();
var
i: integer;
v: string;
begin
try
v:=parEx('var');
result:=getVar(v);
except result:=pars[pars.count-1] end;
i:=0;
while i < pars.count-2 do
begin
result:=replaceText(result, pars[i], pars[i+1]);
inc(i, 2);
end;
if v = '' then exit;
setVar(v, result);
result:='';
end; // replace
procedure dialog();
const
STR2CODE: array [1..7] of string = (
'okcancel=1',
'yesno=4',
'yesnocancel=3',
'error=16',
'question=32',
'warning=48',
'information=64'
);
var
i, j, code: integer;
decode: TStringDynArray;
s: string;
buttons, icon: boolean;
begin
decode:=split(' ',par(1));
code:=0;
for i:=0 to length(decode)-1 do
for j:=1 to length(STR2CODE) do
begin
s:=STR2CODE[j];
if ansiStartsStr(decode[i], s) then
inc(code, strToIntDef(substr(s, 1+pos('=',s)), 0));
end;
buttons:=code AND 15 > 0;
icon:=code SHR 4 > 0;
if not icon and buttons then
inc(code, MB_ICONQUESTION);
case msgDlg(p, code, par(2)) of
MRYES, MROK: result:=if_(buttons, '1'); // if only OK button is available, then return nothing
MRCANCEL: result:=if_(code and MB_YESNOCANCEL = MB_YESNOCANCEL, 'cancel'); // for the YESNOCANCEL, we return cancel to allow to tell NO from CANCEL
else result:='';
end;
end; // dialog
procedure setAccount();
var
a: Paccount;
s: string;
begin
result:='';
if p > '' then
a:=getAccount(p, TRUE)
else
a:=md.cd.account;
if a = NIL then exit;
spaceIf(TRUE);
try
s:=parEx('password');
if validUsername(s, TRUE) then
a.pwd:=s;
except end;
try
s:=parEx('newname');
if validUsername(s) then
a.user:=s;
except end;
try a.redir:=parEx('redirect') except end;
try a.noLimits:=isTrue(parEx('no limits')) except end;
try a.enabled:=isTrue(parEx('enabled')) except end;
try a.group:=isTrue(parEx('is group')) except end;
try a.link:=split(';',parEx('member of')) except end;
try addArray(a.link, split(';',parEx('add member of'))) except end;
try removeArray(a.link, split(';',parEx('remove member of'))) except end;
try a.notes:=parEx('notes') except end;
try a.notes:=setKeyInString(a.notes, parEx('notes key')) except end;
end; // setAccount
procedure getterAccount();
var
a: Paccount;
s: string;
begin
result:='';
if p > '' then
a:=getAccount(p, TRUE)
else
a:=md.cd.account;
if a = NIL then exit;
s:=lowercase(par(1));
if s = 'redirect' then result:=a.redir
else if s = 'no limits' then trueIf(a.nolimits)
else if s = 'enabled' then trueIf(a.enabled)
else if s = 'is group' then trueIf(a.group)
else if s = 'member of' then result:=join(';',a.link)
else if s = 'notes' then result:=a.notes
else if s = 'password' then result:=a.pwd
else if s = 'password is' then trueIf((a.pwd=pars[2]) or (trim(a.pwd)=par(2))) //add by mars
else if s = 'exists' then result:='1';
try result:=getKeyFromString(a.notes, parEx('notes key')) except end;
end; // getterAccount
procedure newAccount();
var
a: Taccount;
begin
result:='';
if accountExists(p, TRUE) then exit; // username already in use
if not validUsername(p) then exit;
fillchar(a, sizeof(a), 0); // the account is disabled by default
a.user:=p;
setLength(accounts, length(accounts)+1);
accounts[length(accounts)-1]:=a;
setAccount();
end; // newAccount
function fromTable(tbl, k:string):string;
var
i: integer;
space, h: THashedStringList;
s: string;
begin
result:='';
if tbl = 'ini' then deprecatedMacro('from table|ini','from table|#ini');
try space:=getVarSpace(tbl);
except exit end;
if not satisfied(space) then exit;
i:=space.indexOfName(tbl);
if (i < 0) and ansiStartsStr('$', tbl) then
begin
s:=md.tpl[copy(tbl,2,MAXINT)];
if s = '' then exit;
i:=space.add(tbl+'='+s);
end;
if i < 0 then exit;
// the text of the table is converted to a hashed structure, and cached through the objects[] property
h:=space.objects[i] as THashedStringList;
if h = NIL then
begin
h:=ThashedStringList.create();
h.text:=space.valueFromIndex[i];
space.objects[i]:=h;
end;
result:=h.values[k];
// we are reading a value from the ini, so we convert the 'no' to a valid false value (the empty string)
if stringExists(tbl, ['ini','#ini']) and (result = 'no') then result:='';
end; // fromTable
procedure setTable();
var
i: integer;
k, v: string;
space, h: THashedStringList;
begin
result:='';
space:=getVarSpace(p);
if not satisfied(space) then exit;
// set the table variable as text
v:=par(1);
space.values[p]:=nonEmptyConcat('', space.values[p], CRLF)+v;
// access the table object
i:=space.indexOfName(p);
h:=space.objects[i] as THashedStringList;
if h = NIL then
begin
h:=ThashedStringList.create();
space.objects[i]:=h;
end;
// fill the object
k:=chop('=',v);
v:=macroDequote(v);
h.values[k]:=v;
end; // setTable
procedure disconnect();
var
i: integer;
ipmask, portmask: string;
begin
if pars.count = 0 then
begin
if satisfied(md.cd) then
md.cd.conn.disconnect();
exit;
end;
ipmask:=par(0,'ip');
portmask:=par(1,'port');
if ipmask = '' then exit;
for i:=0 to srv.conns.count-1 do
with conn2data(i) do
if addressmatch(ipmask, address)
and ((portmask = '') or filematch(portmask, conn.port)) then
conn.disconnect();
result:='';
end; // disconnect
procedure vardomain();
var
space: ThashedStringList;
sep: string;
i: integer;
fs: TfastStringAppend;
values: boolean;
begin
fs:=TfastStringAppend.create;
try
values:=sameText(par('get'), 'values');
sep:=par('separator', FALSE, '|');
space:=getVarSpace(p);
for i:=0 to space.count-1 do
if ansiStartsText(p, space.names[i]) then
begin
if fs.length > 0 then
fs.append(sep);
if values then
fs.append(space.valueFromIndex[i])
else
fs.append(space.names[i]);
end;
result:=fs.get();
finally fs.free end;
end; // vardomain
procedure exec_();
var
s: string;
code: cardinal;
begin
s:=macroDequote(par(1));
if fileOrDirExists(s) then
s:=quoteIfAnyChar(' ', s)
else
if unnamedPars < 2 then
s:='';
if parExist(['out']) or parExist(['timeout']) or parExist(['exit code']) then
try
spaceIf(captureExec(macroDequote(p)+nonEmptyConcat(' ', s), s, code, parF('timeout',2)));
try setVar(parEx('exit code'), intToStr(code)) except end;
setVar(parEx('out'), s);
except end
else
spaceIf(exec(macroDequote(p), s))
end; // exec_
procedure memberOf();
var
a: Paccount;
s: string;
begin
result:='';
s:=par(1, 'user');
if s > '' then
a:=getAccount(s, TRUE)
else if assigned(md.cd) then
a:=md.cd.account
else
exit;
s:=par(0,'group');
if s = '' then // you don't tell me the group, i'll tell you the groups
begin
result:=join(';',expandLinkedAccounts(md.cd.account));
exit;
end;
a:=findEnabledLinkedAccount(a, split(';',s));
if assigned(a) then result:=a.user;
end; // memberOf
procedure canArchive(f:Tfile);
begin trueIf(assigned(f) and f.hasRecursive(FA_ARCHIVABLE) or (f = NIL) and md.archiveAvailable) end;
procedure actionAllowed(action:TfileAction);
var
f: Tfile;
local: boolean;
begin // note: "delete" is meant for files inside the folder bearing the permission
local:=FALSE;
result:='';
try
f:=mainfrm.findFileByURL(parEx('path'), md.folder);
if f = NIL then exit;
local:=TRUE;
except
if action = FA_ACCESS then f:=md.f
else f:=md.folder;
end;
trueIf(accountAllowed(action, md.cd, f));
if local then
freeIfTemp(f);
end; // actionAllowed
procedure cookie();
function timeForCookies(v:string):string;
var
t: Tdatetime;
begin
try
if charInSet(getFirstChar(v), ['+','-']) then
t:=now()+strToFloat(v)
else
try t:=maybeUnixTime(strToFloat(v));
except t:=strToDateTime(v) end;
result:=dateToHTTP(localToGMT(t));
except result:=v end;
end; // timeForCookies
function getPairs():TStringDynArray;
var
i: integer;
k, v: string;
begin
result:=NIL;
for i:=1 to pars.count-1 do
begin
v:=pars[i];
k:=trim(chop('=', v));
v:=trim(v);
if k = 'value' then // this is handled below
continue
else if k = 'expires' then
v:=timeForCookies(v);
addArray(result, [k, v]);
end;
end; // getPairs
begin
if not satisfied(md.cd) then exit;
result:='';
try md.cd.conn.setCookie(p, parEx('value'), getPairs());
except result:=noMacrosAllowed(md.cd.conn.getCookie(p)) end; // there was no "value" to set, so just read
end; // cookie
procedure regexp();
var
subs: TStringDynArray;
subj, s, mods: string;
i: integer;
begin
// input from variable or text
try subj:=getVar(parEx('var'));
except subj:=par(1) end;
// check
mods:='m'+if_(isFalse(par('case')),'i');
p:=macroDequote(p);
i:=reMatch(subj, p, mods, 1, @subs);
if i <= 0 then
begin
result:='';
exit;
end;
// return first match, or position
if assigned(subs) then
result:=subs[min(length(subs)-1,1)]
else
result:=intToStr(i);
// eventually communicate matched substrings
try
parEx('sub');
s:='';
for i:=0 to length(subs)-1 do
s:=s+format('%d=%s'+CRLF, [i, subs[i]]);
setVar(parEx('sub'), s);
except end;
try
result:=reReplace(subj, p, parEx('replace'), mods);
setVar(parEx('var'), result); // we put the output where we got the input
result:='';
except end;
end; // regexp
procedure dir();
var
sr: TSearchRec;
fs: TfastStringAppend;
sep, s: string;
begin
result:='';
// user can specify a file mask, or a folder path
s:=excludeTrailingPathDelimiter(p);
if directoryExists(s) then
s:=s+'\*';
if findfirst(s, faAnyFile, sr) <> 0 then exit;
sep:=par('separator', FALSE, '|');
try
fs:=TfastStringAppend.create();
repeat
if (sr.name = '.') or (sr.name = '..') then continue;
if fs.length > 0 then
fs.append(sep);
fs.append(sr.name);
until findNext(sr) <> 0;
result:=fs.get();
finally
findClose(sr);
freeAndNIL(fs);
end;
end; // dir
procedure handleSymbol();
var
s, usr: string;
i: integer;
begin
// search for the symbol in the translation table
i:=length(md.table)-2;
if odd(i) then dec(i); // ensure the table has a legal length
while (i >= 0) and not sameText(md.table[i], name) do
dec(i,2);
if i >= 0 then
begin
result:=md.table[i+1];
exit;
end;
result:=name; // by default, an unrecognized symbol remains the same (just as the song)
// most symbols here, are here because they can be heavy to calculate, so we ensure we do
// it only upon request. others are for centralization.
if ansiStartsText('%sym-', name) then // legacy: surpassed by {.section.}
result:=tpl[substr(name,2,-1)]
else if name = '%item-icon%' then
result:=first(getItemIcon(md.f), name)
else if name = '%item-archive%' then
if assigned(md.f) and assigned(md.tpl) and md.f.hasRecursive(FA_ARCHIVABLE) then result:=md.tpl['item-archive']
else result:=''
else if name = '%item-dl-count%' then
if md.f = NIL then result:=''
else result:=intToStr(md.f.DLcount)
else if name = '%connections%' then
result:=intToStr(srv.conns.count)
else if name = '%style%' then
result:=tpl['style']
else if name = '%timestamp%' then
result:=dateTimeToStr(now())
else if name = '%date%' then
result:=dateToStr(now())
else if name = '%time%' then
result:=timeToStr(now())
else if name = '%now%' then
result:=floatToStr(now())
else if name = '%version%' then
result:=main.VERSION
else if name = '%build%' then
result:=VERSION_BUILD
else if name = '%uptime%' then
result:=uptimestr()
else if name = '%speed-out%' then
result:=floatToStrF(srv.speedOut/1000, ffFixed, 7,2)
else if name = '%speed-in%' then
result:=floatToStrF(srv.speedIn/1000, ffFixed, 7,2)
else if name = '%total-out%' then
result:=smartSize(outTotalOfs+srv.bytesSent)
else if name = '%total-in%' then
result:=smartSize(inTotalOfs+srv.bytesReceived)
else if name = '%total-downloads%' then
result:=intToStr(downloadsLogged)
else if name = '%total-hits%' then
result:=intToStr(hitsLogged)
else if name = '%total-uploads%' then
result:=intToStr(uploadsLogged)
else if name = '%number-addresses%' then
result:=intToStr(countIPs())
else if name = '%number-addresses-ever%' then
result:=intToStr(ipsEverConnected.count)
else if name = '%number-addresses-downloading%' then
result:=intToStr(countIPs(TRUE))
else if name = '%number-users%' then
result:=intToStr(countIPs(FALSE, TRUE))
else if name = '%number-users-downloading%' then
result:=intToStr(countIPs(TRUE, TRUE))
else if name = '%cwd%' then
result:=getCurrentDir()
else if name = '%port%' then
result:=srv.port;
if assigned(md.cd) then
begin
usr:=md.cd.user;
if name = '%host%' then
result:=getSafeHost(md.cd)
else if name = '%ip%' then
result:=md.cd.address
else if name = '%ip-to-name%' then
result:=localDNSget(md.cd.address)
else if name = '%lang%' then
result:=stripChars(copy(md.cd.conn.getHeader('Accept-Language'),1,2), ['a'..'z','A'..'Z'], TRUE)
else if name = '%url%' then
result:=macroQuote(md.cd.conn.request.url)
else if name = '%user%' then
result:=macroQuote(usr)
else if name = '%password%' then
result:=macroQuote(md.cd.conn.request.pwd)
else if name = '%loggedin%' then
result:=if_(usr>'', tpl['loggedin'])
else if name = '%login-link%' then
result:=if_(usr='', tpl['login-link'])
else if name = '%user-notes%' then
if md.cd.account = NIL then result:=''
else result:=md.cd.account.notes
else if name = '%stream-size%' then
result:=intToStr(md.cd.conn.bytesFullBody)
else if name = '%is-archive%' then
trueIf(md.cd.downloadingWhat=DW_ARCHIVE)
end;
if assigned(md.folder) then
if name = '%folder-item-comment%' then
result:=md.folder.getDynamicComment()
else if name = '%folder-comment%' then
begin
result:=md.folder.getDynamicComment();
if result > '' then
result:=replaceText(tpl['folder-comment'], '%item-comment%', result)
end
else if name = '%diskfree%' then
result:=smartSize(diskSpaceAt(md.folder.resource)-minDiskSpace*MEGA)
else if name = '%up%' then
result:=if_(assigned(md.tpl) and not md.folder.isRoot(), md.tpl['up'])
else if name = '%encoded-folder%' then
result:=md.folder.url(TRUE)
else if name = '%parent-folder%' then
result:=md.folder.parentURL()
else if name = '%folder-name%' then
result:=md.folder.name
else if name = '%folder-resource%' then
result:=md.folder.resource
else if name = '%folder%' then
with md.folder do result:=if_(isRoot(), '/', getFolder()+name+'/')
;
if assigned(md.f) then
if name = '%item-name%' then
begin
s:=md.f.name;
if md.hideExt and md.f.isFile() then
setLength(s, length(s)-length(extractFileExt(s)) );
result:=htmlEncode(macroQuote(s))
end
else if name = '%item-type%' then
if md.f.isLink() then
result:='link'
else if md.f.isFolder() then
result:='folder'
else
result:='file'
else if name = '%item-size-b%' then
result:=intToStr(md.f.size)
else if name = '%item-size-kb%' then
result:=intToStr(md.f.size div KILO)
else if name = '%item-size%' then
result:=smartsize(md.f.size)
else if name = '%item-resource%' then
result:=macroQuote(md.f.resource)
else if name = '%item-ext%' then
result:=macroQuote(copy(extractFileExt(md.f.name), 2, MAXINT))
else if name = '%item-added-dt%' then
result:=floatToStr(md.f.atime)
else if name = '%item-modified-dt%' then
result:=floatToStr(md.f.mtime)
// these twos are actually redundant, {.time||when=%item-added-dt%.}
else if name = '%item-added%' then
result:=datetimeToStr(md.f.atime)
else if name = '%item-modified%' then
result:=if_(md.f.mtime=0, 'error', datetimeToStr(md.f.mtime))
else if name = '%item-comment%' then
result:=md.f.getDynamicComment(TRUE)
else if name = '%item-url%' then
result:=macroQuote(md.f.url())
;
if assigned(md.f) and assigned(md.tpl) then
if name = '%new%' then
result:=if_(md.f.isNew(), md.tpl['newfile'])
else if name = '%comment%' then
result:=if_(md.f.getDynamicComment(TRUE) > '', md.tpl['comment'])
;
if assigned(md.tpl) then
if name = '%archive%' then
result:=if_(md.archiveAvailable, md.tpl['archive']);
if ansiContainsText(name, 'folder') and not ansiContainsText(name, 'comment') then
result:=macroQuote(result);
end; // handleSymbol
function stringTotrayMessageType(s:string):TtrayMessageType;
begin
if compareText(s,'warning') = 0 then
result:=TM_WARNING
else if compareText(s,'error') = 0 then
result:=TM_ERROR
else if compareText(s,'info') = 0 then
result:=TM_INFO
else
result:=TM_NONE
end; // stringTotrayMessageType
function renameIt(src,dst:string):boolean;
var
srcReal, dstReal: string;
begin
srcReal:=uri2diskMaybe(src,NIL,FALSE);
dstReal:=uri2diskMaybeFolder(dst);
if isExtension(srcReal, '.lnk')
and not isExtension(src, '.lnk') then
dstReal:=dstReal+'.lnk';
if extractFilePath(dstReal)='' then
dstReal:=extractFilePath(srcReal)+dstReal;
result:=renameFile(srcReal, dstReal)
end; // renameIt
var
i64: int64;
i: integer;
r: Tdatetime;
s: string;
begin
try
assert(assigned(cbData), 'cbMacros: cbData=NIL');
md:=cbData;
if md.breaking then exit;
try
name:=fullmacro;
if (name[1] = '%') and (name[length(name)] = '%') then
begin
handleSymbol();
exit;
end;
if not mainfrm.enableMacrosChk.checked then
exit(fullMacro);
if pars.count = 0 then exit;
// extract first parameter as 'name'
name:=trim(pars[0]);
pars.delete(0); // this operation is done with a memory move over pointers. Having few parameters normally, it's fast. We may eventually avoid this deletion and consider parameters starting from 1.
if name = '' then exit;
macroError('not supported or illegal parameters');
// eventually remove trailing
if pars.Count > 0 then
begin
p:=pars[pars.count-1];
if ansiEndsText('/'+name, p) then
begin
setLength(p, length(p)-length(name)-1);
pars[pars.count-1]:=p;
end;
end;
unnamedPars:=0;
for i:=0 to pars.count-1 do
begin
pars[i]:=replaceStr(pars[i], '{:|:}','|');
if (i = unnamedPars) and (pos('=',pars[i]) = 0) then
inc(unnamedPars);
end;
// handle aliases
if assigned(md.aliases) then
begin
s:=md.aliases.values[name];
if s > '' then
begin
if not ansiStartsStr(MARKER_OPEN, s) then
s:=MARKER_OPEN+s+MARKER_CLOSE;
call(s);
exit;
end;
end;
// here we try to handle some shortcuts.
// it's a special starting character that identifies the macro, and the rest of the name is a parameter.
p:=copy(name,2,MAXINT);
if name[1] = '$' then
section(0);
if name[1] = '!' then
// we look for they key (p) in {.^special:strings.} then in [special:strings]. If no luck, we try to output an eventual parameter, or the key itself.
try result:=first([fromTable('special:strings',p), md.tpl.getStrByID(p), par(0), p]) except end;
if name[1] = '^' then
try call(getVar(p), 0) except end;
if name[1] = '?' then // shortcut for 'urlvar'
result:=urlvar(p);
p:=par(0); // a handy shortcut for the first parameter
// comment is for comments, or if you just want to trash the output of a macro.
// Careful: the comment itself (the parameter of this command) is executed as anything else, unless it's {:quoted:}
if name = 'comment' then
begin
result:='';
exit;
end;
// infix operators are macros in the form PARAMETER NAME PARAMETER. it's handy for comparisons.
infixOperators(['>=','<=','<>','!=','=','>','<']); // the order is important, because >= would be confused with =
// ok, fom here we have macros in the straight form NAME|PARAM|PARAM
name:=ansiLowercase(name);
if name = 'count' then
if satisfied(md.cd) then
result:=intToStr(md.cd.tplCounters.incInt(p)-1); // it can work even with no parameters
if name = 'time' then
begin
s:=par(0,'format');
r:=parF('when',now())+parF('offset',0);
if s = 'y' then result:=floatToStr(r)
else datetimeToString(result, first(s,'c'), r );
end;
if name = 'disconnect' then
disconnect();
if name = 'stop server' then
stopServer();
if name = 'start server' then
startServer();
if name = 'focus' then
begin
application.restore();
application.bringToFront();
result:='';
end;
if name = 'current downloads' then
result:=intToStr( countDownloads( par('ip'), par('user'), if_(sameText(par('file'), 'this'), md.f) as Tfile) );
if name = 'disconnection reason' then
begin
try
if isFalse(parEx('if')) then
begin
result:='';
exit;
end;
except end;
result:=md.cd.disconnectReason; // return the previous state
if pars.count > 0 then md.cd.disconnectReason:=p;
end;
if name = 'clipboard' then
if p = '' then
result:=clipboard.asText
else
begin
try setClip(getVar(parEx('var')))
except setClip(p) end;
result:='';
end;
if name = 'save vfs' then
begin
mainfrm.saveVFS(first(p,lastFileOpen));
result:='';
end;
if name = 'save cfg' then
begin
if p = 'file' then savemode:=SM_FILE
else if p = 'registry' then savemode:=SM_USER
else if p = 'global registry' then savemode:=SM_SYSTEM;
mainFrm.saveCFG();
result:='';
end;
if name = 'js encode' then
result:=jsEncode(p, first(par(1),'''"'));
if name = 'base64' then
result:=string(base64encode(UTF8encode(p)));
if name = 'base64decode' then
result:=utf8ToString(base64decode(ansistring(p)));
if name = 'md5' then
result:=strMD5(p);
if name = 'sha1' then
result:=strSHA1(p);
if name = 'vfs select' then
if pars.count = 0 then
try result:=selectedFile.url()
except result:='' end
else if p = 'next' then
if selectedFile = NIL then
spaceIf(FALSE)
else
begin
with mainFrm.filesBox do selected:=selected.getNext();
spaceIf(TRUE);
end
else
try
s:=parEx('path');
spaceIf(FALSE);
mainFrm.filesBox.selected:= mainFrm.findFilebyURL(s, NIL, FALSE).node;
spaceIf(TRUE);
except end;
if name = 'break' then
begin
result:='';
try
if isFalse(parEx('if')) then
exit;
except end;
try result:=parEx('result') except end;
md.breaking:=TRUE;
exit;
end;
if pars.Count < 1 then exit; // from here, only macros with parameters
if name = 'var domain' then
vardomain();
if name = 'dir' then
dir();
if name = 'no pipe' then
result:=replaceStr(substr(fullMacro, '|'), '|','{:|:}');
if name = 'pipe' then
result:=replaceStr(substr(fullMacro, '|'), '{:|:}','|');
if name = 'add to log' then
begin
try s:=getVar(parEx('var'))
except s:=p end;
mainfrm.add2log(s, md.cd, stringToColorEx(par(1,'color'), clDefault));
result:='';
end;
if name = 'mkdir' then
begin
s:=trim(uri2diskMaybeFolder(p));
spaceIf(not directoryExists(s) and forceDirectory(s));
end;
if name = 'chdir' then
begin
IOresult;
setCurrentDir(p);
spaceIf(IOresult=0);
end;
if name = 'encodeuri' then
encodeuri();
if name = 'decodeuri' then
result:=decodeURL(ansistring(p));
if name = 'set cfg' then
trueIf(mainfrm.setcfg(p));
if name = 'dialog' then
dialog();
if name = 'any macro marker' then
trueIf(anyMacroMarkerIn(first(loadTextfile(uri2diskMaybe(p)), p)));
if name = 'random' then
result:=randomFrom(listToArray(pars));
if name = 'random number' then
if pars.count = 1 then
result:=intToStr(random(1+parI(0)))
else
result:=intToStr(parI(0)+random(1+parI(1)-parI(0)));
if (name = 'force ansi') or (name = 'maybe utf8') then // pre-unicode legacy
result:=p;
if name = 'after the list' then
if md.afterTheList then
result:=macroDequote(p)
else
result:=MARKER_OPEN+fullMacro+MARKER_CLOSE;
if name = 'breadcrumbs' then
breadcrumbs();
if name = 'filename' then
result:=substr(p, lastDelimiter('\/:',p)+1);
if name = 'filepath' then
begin
i:=lastDelimiter('\/:',p);
if i = 0 then
result:=''
else
result:=substr(p, 1, i);
end;
if name = 'not' then
trueIf(isFalse(p));
if name = 'length' then
begin // don't trim
try s:=getVar(parEx('var', FALSE))
except s:=pars[0] end;
result:=intToStr(length(s));
end;
if name = 'load' then
load(p, par(1,'var'));
if name = 'load tpl' then
if satisfied(md.cd) then
begin
md.cd.tpl:=cachedTpls.getTplFor(p);
result:='';
end;
if name = 'filesize' then
begin
if reMatch(p, '^https?://', 'i!') > 0 then i64:=httpFileSize(p)
else i64:=sizeOfFile(uri2diskMaybe(p));
result:=intToStr(max(0,i64)); // we return 0 instead of -1 for non-existent files, t
end;
if name = 'filetime' then
result:=floatToStr(getMtime(p));
if name = 'header' then
if satisfied(md.cd) then
try result:=noMacrosAllowed(md.cd.conn.getHeader(ansistring(p))) except end;
if name = 'urlvar' then
result:=urlvar(p);
if name = 'postvar' then
if satisfied(md.cd) then
try
result:=noMacrosAllowed(md.cd.postVars.values[p]);
setVar(parEx('var'), result); // if no var is specified, it will break here, and result will have the value
result:='';
except end;
if name = 'section' then
section(1);
if name = 'trim' then
result:=p;
if name = 'lower' then
result:=ansiLowercase(p);
if name = 'upper' then
result:=ansiUppercase(p);
if name = 'abs' then
result:=floatToStr(abs(parF(0)));
if name = 'upload failed' then
begin
md.cd.uploadFailed:=p;
result:='';
end;
if name = 'is file protected' then
result:=if_(filematch(PROTECTED_FILES_MASK, parVar('var',0)), '1');
if name = 'get' then
try
if p = 'can recur' then trueIf(mainFrm.recursiveListingChk.Checked)
else if p = 'can upload' then actionAllowed(FA_UPLOAD)
else if p = 'can delete' then actionAllowed(FA_DELETE)
else if p = 'can access' then actionAllowed(FA_ACCESS)
else if p = 'can archive' then canArchive(md.folder)
else if p = 'can archive item' then canArchive(md.f)
else if p = 'url' then getUri()
else if p = 'stop spiders' then trueIf(mainFrm.stopSpidersChk.checked)
else if p = 'is new' then trueIf(md.f.isNew())
else if p = 'agent' then result:=md.cd.agent
else if p = 'tpl file' then result:=tplFilename
else if p = 'protocolon' then result:=protoColon()
else if p = 'speed limit' then result:=intToStr(round(speedLimit))
else if p = 'external address' then
begin
if externalIP = '' then getExternalAddress(externalIP);
result:=externalIP;
end
else if p = 'accounts' then
result:=join(';', getAccountList(
stringExists(par(1),['','users']),
stringExists(par(1),['','groups'])
))
;
except unsatisfied() end;
if name = 'call' then
try call(getVar(p), 1) except end;
if name = 'inc' then
inc_();
if name = 'dec' then
inc_(-1);
if name = 'chr' then
begin
result:='';
for i:=0 to pars.count-1 do
try result:=result+chr(strToInt(replaceStr(pars[i],'x','$')))
except end;
end;
if name = 'dequote' then
result:=macroDequote(p);
if name ='quote' then
begin
p:=macroDequote(p);
applyMacrosAndSymbols(p, cbMacros, cbData);
result:=macroQuote(p);
end;
if name = 'encode html' then
result:=htmlEncode(p);
if name = 'play' then
begin
result:='';
playSound(Pchar(p), 0, SND_ALIAS or SND_ASYNC or SND_NOWAIT);
end;
if name = 'delete' then
begin
s:=uri2diskMaybe(p,NIL,FALSE);
if isTrue(par('bin',TRUE,'1')) then
spaceIf(moveToBin(s, isTrue(par('forced'))))
else
spaceIf(deltree(s));
end;
if name = 'disk free' then
result:=intToStr(diskSpaceAt(uri2diskMaybe(p)));
if name = 'vfs to disk' then
if isAbsolutePath(p) then
result:=p
else if dirCrossing(p) and not ansiStartsStr('/', p) then
result:=expandFileName(includeTrailingPathDelimiter(md.folder.resource)+p)
else
result:=uri2disk(p, md.folder);
if name = 'exists' then
if ansiContainsStr(p, '/') then
trueIf(fileExistsByURL(p))
else
trueIf(fileOrDirExists(p));
if name = 'is file' then
trueIf(fileExists(p));
if name = 'mime' then
begin
result:='';
if satisfied(md.cd) then
md.cd.conn.reply.contentType:=ansistring(p);
end;
if name = 'calc' then
result:=floatToStr(evalFormula(p));
if name = 'smart size' then
result:=smartsize(strToInt64(p));
if name = 'round' then
result:=floatToStr(roundTo(parF(0), -parI(1, 0)));
if name = 'md5 file' then
result:=createFingerprint(p);
if name = 'exec' then
exec_();
if name = 'set speed limit for address' then
begin
if pars.count = 1 then
setSpeedLimitIP(parF(0))
else
with objByIp(p) do
begin
limiter.maxSpeed:=round(parF(1)*1000);
customizedLimiter:=TRUE;
end;
result:='';
end;
if name = 'set speed limit for connection' then
if satisfied(md.cd) then
try
if assigned(md.cd.limiter) then
begin
md.cd.limiter.maxSpeed:=round(parF(0)*1000);
exit;
end;
md.cd.limiter:=TspeedLimiter.create(round(parF(0)*1000));
md.cd.conn.limiters.add(md.cd.limiter);
srv.limiters.add(md.cd.limiter);
result:='';
except
md.cd.conn.limiters.remove(md.cd.limiter);
srv.limiters.remove(md.cd.limiter);
freeAndNIL(md.cd.limiter);
result:='';
end;
if name = 'member of' then
memberOf();
if name = 'add header' then
if satisfied(md.cd) then
begin
result:='';
// macro 'mime' should be used for content-type, but this test will save precious time to those who will be fooled by the presence this macro
if ansiStartsText('Content-Type:', p) then
md.cd.conn.reply.contentType:=ansistring(trim(substr(p, ':')))
else if ansiStartsText('Location:', p) then
with md.cd.conn.reply do
begin
mode:=HRM_REDIRECT;
url:=UTF8encode(trim(substr(p, ':')))
end
else
md.cd.conn.addHeader(ansistring(p), isTrue(par('overwrite',true,'1')));
end;
if name = 'remove header' then
if satisfied(md.cd) then
begin
result:='';
md.cd.conn.removeHeader(ansistring(p));
end;
if name = 'get ini' then
result:=getKeyFromString(mainFrm.getCfg(), p);
if name = 'set ini' then
begin
result:='';
mainfrm.setCfg(p);
end;
if name = 'set' then
begin
try s:=getVar(parEx('var'));
except
if pars.count < 2 then s:=''
else s:=macroDequote(pars[1]);
end;
if par('mode') = 'append' then
s:=getVar(p)+s
else if par('mode') = 'prepend' then
s:=s+getVar(p);
spaceIf(setVar(p, s));
end;
if name = 'notify' then
begin
tray.balloon(p, parF('timeout',3), stringTotrayMessageType(par('type')), par('title'));
result:='';
end;
if name = 'cookie' then
cookie();
if name = 'new account' then
newAccount();
if name = 'delete account' then
begin
deleteAccount(p);
result:='';
end;
if name = 'delete item' then
deleteItem();
if pars.count < 2 then exit; // from here, only macros with at least 2 parameters
if name = 'set item' then
setItem();
if name = 'get item' then
getItem();
if name = 'while' then
while_();
if name = 'set table' then
setTable();
if name = 'add folder' then
addFolder();
if (name = 'save') or (name = 'append') then
save();
if name = 'rename' then
begin
spaceIf( not isExtension(par(1), '.lnk') and // security matters (by mars)
renameIt(p, par(1)) );
if (result > '') and not stopOnMacroRename then // should we stop recursion?
try
// by default, we'll stop after first stacked [on macro rename], but recursive=1 will remove this limit
stopOnMacroRename:=isFalse(par('recursive'));
runEventScript('on macro rename', toSA(['%old-name%',p,'%new-name%',par(1)]), md.cd);
finally
stopOnMacroRename:=FALSE;
end;
end;
if name = 'move' then
begin
s:=uri2diskMaybeFolder(par(1));
spaceIf((s>'') and movefile(uri2diskMaybe(p,NIL,FALSE), s));
end;
if name = 'copy' then
spaceIf(copyfile(uri2diskMaybe(p), uri2diskMaybeFolder(par(1))));
if name = 'from table' then
result:=fromTable(p, par(1));
if name = 'match' then
trueIf(filematch(p, par(1)));
if name = 'match address' then
trueIf(addressmatch(p, par(1)));
if name = 'regexp' then
regexp();
if name = 'pos' then
result:=intToStr(pos_(isTrue(par('case')), parVar('what', 0), parVar('var', 1), strToIntDef(par('from'), 1)));
if name = 'repeat' then
result:=dupeString(macroDequote(par(1)), strToIntDef(p,1));
if name = 'count substring' then
result:=intToStr(countSubstr(pars[0], par(1)));
if name = 'and' then
allLogic(TRUE);
if name = 'or' then
allLogic(FALSE);
if name = 'xor' then
trueIf(isTrue(p) xor isTrue(par(1)));
if name = 'add' then
result:=floatToStr(parF(0)+parF(1));
if name = 'sub' then
result:=floatToStr(parF(0)-parF(1));
if name = 'mul' then
result:=floatToStr(parF(0)*parF(1));
if name = 'div' then
result:=floatToStr(safeDiv(parF(0),parF(1)));
if name = 'mod' then
result:=intToStr(safeMod(parI(0),parI(1)));
if stringExists(name, ['min','max']) then
minOrMax();
if stringExists(name, ['if','if not']) then
begin
try p:=getVar(parEx('var'));
except end;
if isTrue(p) xor (length(name) = 2) then result:=macroDequote(par(2))
else result:=macroDequote(par(1));
end;
if stringExists(name, ['=','>','>=','<','<=','<>','!=']) then
trueIf(compare(name, p, par(1)));
if name = 'switch' then
switch();
if name = 'set account' then
setAccount();
if name = 'get account' then
getterAccount();
if name = 'cut' then
cut();
if name ='for line' then
forLine();
if pars.count < 3 then exit; // from here, only macros with at least 3 parameters
if name ='for each' then
foreach();
if name = 'substring' then
substring();
if name = 'replace' then
replace();
if name = 'convert' then
convert();
if pars.count < 4 then exit;
if name = 'for' then
for_();
finally
if mainfrm.macrosLogChk.checked then
begin
if not fileExists(MACROS_LOG_FILE) then
saveTextFile(MACROS_LOG_FILE, HEADER);
macrosLog(fullMacro, result, md.logTS);
md.logTS:=FALSE;
end;
end;
except
if mainfrm.macrosLogChk.checked then
macrosLog(fullMacro, 'Exception, please report this bug on www.rejetto.com/forum/');
result:='';
end;
end; // cbMacros
function tryApplyMacrosAndSymbols(var txt:string; var md:TmacroData; removeQuotings:boolean=true):boolean;
var
s: string;
begin
result:=FALSE;
try
md.aliases:=defaultalias; // we don't even create a new object if not necessary
if assigned(md.tpl) then
begin
s:=md.tpl['special:alias'];
if s > '' then
begin
md.aliases:=THashedStringList.create;
md.aliases.text:=s;
md.aliases.addStrings(defaultAlias);
end;
end;
if md.cd = NIL then
begin
md.tempVars:=THashedStringList.create;
end;
md.logTS:=TRUE;
md.breaking:=FALSE;
try
applyMacrosAndSymbols(txt, cbMacros, @md, removeQuotings);
result:=TRUE;
except
on e:EtplError do mainFrm.setStatusBarText(format('Template error at %d,%d: %s: %s ...', [e.row,e.col,e.message,e.code]), 1000);
on Exception do raise;
end;
finally
if md.aliases <> defaultAlias then
freeAndNIL(md.aliases);
freeAndNIL(md.tempVars);
end;
end; // tryApplyMacrosAndSymbols
function runScript(script:string; table:TstringDynArray=NIL; tpl_:Ttpl=NIL; f:Tfile=NIL; folder:Tfile=NIL; cd:TconnData=NIL):string;
var
md: TmacroData;
begin
result:=trim(script);
if result = '' then exit;
fillchar(md, sizeOf(md), 0);
md.tpl:=first(tpl_, tpl);
md.f:=f;
md.folder:=folder;
md.cd:=cd;
md.table:=table;
tryApplyMacrosAndSymbols(result, md);
end; // runScript
function runEventScript(event:string; table:TStringDynArray=NIL; cd:TconnData=NIL):string;
begin
addArray(table, ['%event%', event]);
result:=runScript(eventScripts[event], table, eventScripts, NIL, NIL, cd);
end; // runEventScript
initialization
cachedTpls:=TcachedTpls.create();
eventScripts:=Ttpl.create();
defaultAlias:=THashedStringList.create();
defaultAlias.caseSensitive:=FALSE;
defaultAlias.text:=getRes('alias');
staticVars:=THashedStringList.create;
currentCFGhashed:=THashedStringList.create();
with staticVars do
objects[add('ini='+currentCFG)]:=currentCFGhashed;
finalization
freeAndNIL(cachedTpls);
freeAndNIL(eventScripts);
freeAndNIL(defaultAlias);
freeAndNIL(currentCFGhashed);
staticVars.free;
end.