Any missing parameter '+
'would be replaced by its default value.
');
end;
class procedure TExpressionHtmlTableStyle.BeforeValue(WR: TTextWriter);
begin
WR.AddShort(' | ');
end;
class procedure TExpressionHtmlTableStyle.EndTable(WR: TTextWriter);
begin
WR.AddShort('');
end;
class procedure TExpressionHtmlTableStyle.StartTable(WR: TTextWriter);
begin
WR.AddShort('');
end;
class procedure TExpressionHtmlTableStyleBootstrap.AddLabel(
WR: TTextWriter; const text: string; kind: THtmlTableStyleLabel);
const SETLABEL: array[THtmlTableStyleLabel] of string[7] = (
'danger','success','danger','success','primary');
begin
WR.AddShort('');
WR.AddHtmlEscapeString(text);
WR.AddShort('');
end;
class procedure TExpressionHtmlTableStyleBootstrap.StartTable(WR: TTextWriter);
begin
WR.AddShort('');
end;
function TMVCViewsMustache.RegisterExpressionHelpers(
const aNames: array of RawUTF8;
const aEvents: array of TSynMustacheHelperEvent): TMVCViewsMustache;
begin
if self<>nil then
TSynMustache.HelperAdd(fViewHelpers,aNames,aEvents);
result := self;
end;
function TMVCViewsMustache.RegisterExpressionHelpersForTables(
aRest: TSQLRest; const aTables: array of TSQLRecordClass): TMVCViewsMustache;
var t: integer;
begin
if (self<>nil) and (aRest<>nil) then
for t := 0 to high(aTables) do
if aRest.Model.GetTableIndex(aTables[t])>=0 then
TExpressionHelperForTable.Create(aRest,aTables[t],fViewHelpers);
result := self;
end;
function TMVCViewsMustache.RegisterExpressionHelpersForTables(
aRest: TSQLRest): TMVCViewsMustache;
var t: integer;
begin
if (self<>nil) and (aRest<>nil) then
for t := 0 to aRest.Model.TablesMax do
TExpressionHelperForTable.Create(aRest,aRest.Model.Tables[t],fViewHelpers);
result := self;
end;
function TMVCViewsMustache.RegisterExpressionHelpersForCrypto: TMVCViewsMustache;
begin
result := RegisterExpressionHelpers(['md5','sha1','sha256'],[md5,sha1,sha256]);
end;
class procedure TMVCViewsMustache.md5(const Value: variant;
out result: variant);
begin
RawUTF8ToVariant(SynCrypto.MD5(ToUTF8(Value)),result);
end;
class procedure TMVCViewsMustache.sha1(const Value: variant;
out result: variant);
begin
RawUTF8ToVariant(SynCrypto.SHA1(ToUTF8(Value)),result);
end;
class procedure TMVCViewsMustache.sha256(const Value: variant;
out result: variant);
begin
RawUTF8ToVariant(SynCrypto.SHA256(ToUTF8(Value)),result);
end;
function TMVCViewsMustache.GetRenderer(methodIndex: integer; var view: TMVCView): TSynMustache;
var age: PtrUInt;
begin
if cardinal(methodIndex)>=fFactory.MethodsCount then
raise EMVCException.CreateUTF8('%.Render(methodIndex=%)',[self,methodIndex]);
with fViews[methodIndex], Locker.ProtectMethod do begin
if MethodName='' then
raise EMVCException.CreateUTF8('%.Render(''%''): not a View',[self,MethodName]);
if (Mustache=nil) and (FileName='') then
raise EMVCException.CreateUTF8('%.Render(''%''): Missing Template in ''%''',
[self,MethodName,SearchPattern]);
if (Mustache=nil) or ((fViewTemplateFileTimestampMonitor<>0) and
(FileAgeCheckTickFileAgeLast) then begin
Mustache := nil; // no Mustache.Free: TSynMustache instances are cached
FileAgeLast := age;
Template := GetTemplate(ShortFileName);
if Template<>'' then
try
Mustache := TSynMustache.Parse(Template);
if Mustache.FoundInTemplate(fViewGenerationTimeTag) then
include(Flags,viewHasGenerationTimeTag);
except
on E: Exception do
raise EMVCException.CreateUTF8('%.Render(''%''): Invalid Template: % - %',
[self,ShortFileName,E,E.Message]);
end else
raise EMVCException.CreateUTF8('%.Render(''%''): Missing Template in ''%''',
[self,ShortFileName,SearchPattern]);
if fViewTemplateFileTimestampMonitor<>0 then
FileAgeCheckTick := GetTickCount64+
Int64(fViewTemplateFileTimestampMonitor)*Int64(1000);
end;
end;
view.ContentType := ContentType;
view.Flags := view.Flags+Flags;
result := Mustache;
end;
end;
function TMVCViewsMustache.FindTemplates(const Mask: TFileName): TFileNameDynArray;
begin
result := FindFilesDynArrayToFileNames(
FindFiles(ViewTemplateFolder,Mask,'',{sorted=}false,{withdir=}false));
end;
function TMVCViewsMustache.GetTemplate(const aFileName: TFileName): RawUTF8;
begin
result := AnyTextFileToRawUTF8(ViewTemplateFolder+aFileName,true);
end;
{$WARN SYMBOL_DEPRECATED OFF} // we don't need TDateTime, just values to compare
function TMVCViewsMustache.GetTemplateAge(const aFileName: TFileName): PtrUInt;
begin
result := FileAge(ViewTemplateFolder+aFileName);
end;
{$WARN SYMBOL_DEPRECATED ON}
procedure TMVCViewsMustache.Render(methodIndex: Integer; const Context: variant;
var View: TMVCView);
begin
View.Content := GetRenderer(methodIndex,View).
Render(Context,fViewPartials,fViewHelpers);
if IsVoid(View.Content) then // rendering failure
with fViews[methodIndex] do begin
Locker.Enter;
try
Mustache := nil; // force reload view ASAP
finally
Locker.Leave;
end;
raise EMVCException.CreateUTF8('%.Render(''%''): Void [%] Template - '+
'please write some content into this file!',[self,ShortFileName,FileName]);
end;
end;
{ TMVCSessionAbstract }
constructor TMVCSessionAbstract.Create;
begin
inherited;
end;
function TMVCSessionAbstract.CheckAndRetrieveInfo(PRecordDataTypeInfo: pointer): variant;
var rec: array[byte] of word; // 512 bytes to store locally any kind of record
recsize: integer;
sessionID: integer;
procedure ProcessSession;
var recJSON: RawUTF8;
begin
SaveJSON(rec,PRecordDataTypeInfo,[twoForceJSONExtended,
twoEnumSetsAsBooleanInRecord,twoTrimLeftEnumSets],recJSON);
TDocVariantData(result).InitJSONInPlace(pointer(recJSON),JSON_OPTIONS_FAST);
end;
begin
SetVariantNull(result);
recsize := RecordTypeInfoSize(PRecordDataTypeInfo);
if recsize>SizeOf(rec) then // =0 if PRecordDataTypeInfo=nil (sessionID only)
raise EMVCException.CreateUTF8('%.CheckAndRetrieveInfo: recsize=%',[self,recsize]);
FillCharFast(rec,recsize,0);
try
sessionID := CheckAndRetrieve(@rec,PRecordDataTypeInfo);
if sessionID<>0 then begin
if recsize>0 then
ProcessSession;
_ObjAddProps(['id',sessionID],result);
end;
finally
if recsize>0 then // manual finalization of managed fields
RecordClear(rec,PRecordDataTypeInfo);
end;
end;
{ TMVCSessionWithCookies }
constructor TMVCSessionWithCookies.Create;
var rnd: THash512;
begin
inherited Create;
fContext.CookieName := 'mORMot';
// temporary secret for encryption
fContext.CryptNonce := Random32gsl;
TAESPRNG.Main.FillRandom(@fContext.Crypt,sizeof(fContext.Crypt));
// temporary secret for HMAC-CRC32C
TAESPRNG.Main.FillRandom(@rnd,sizeof(rnd));
fContext.Secret.Init(@rnd,sizeof(rnd));
end;
procedure XorMemoryCTR(data: PCardinal; key256bytes: PCardinalArray;
size: PtrUInt; ctr: cardinal);
begin
while size>=sizeof(Cardinal) do begin
dec(size,sizeof(Cardinal));
data^ := data^ xor key256bytes[ctr and $3f] xor ctr;
inc(data);
ctr := ((ctr xor (ctr shr 15))*2246822519); // prime-number ctr diffusion
ctr := ((ctr xor (ctr shr 13))*3266489917);
ctr := ctr xor (ctr shr 16);
end;
if size=0 then
exit; // no padding
repeat
dec(size);
PByteArray(data)[size] := PByteArray(data)[size] xor ctr;
ctr := ctr shr 8; // 1..3 pending iterations
until size=0;
end;
procedure TMVCSessionWithCookies.Crypt(P: PAnsiChar; bytes: integer);
begin
XorMemoryCTR(@P[4],@fContext.Crypt,bytes-4,xxHash32(fContext.CryptNonce,P,4));
end;
function TMVCSessionWithCookies.Exists: boolean;
begin
result := GetCookie<>'';
end;
type // map the binary layout of our base-64 serialized cookies
TCookieContent = packed record
head: packed record
cryptnonce: cardinal; // ctr=hash32(cryptnonce)
hmac: cardinal; // = signature
session: integer; // = jti claim
issued: cardinal; // = iat claim (from UnixTimeUTC - Y2106)
expires: cardinal; // = exp claim
end;
data: array[0..2047] of byte; // binary serialization of record value
end;
PCookieContent = ^TCookieContent;
function TMVCSessionWithCookies.CheckAndRetrieve(PRecordData: pointer;
PRecordTypeInfo: pointer; PExpires: PCardinal): integer;
var cookie: RawUTF8;
begin
cookie := GetCookie;
if cookie='' then
result := 0 else // no cookie -> no session
result := CheckAndRetrieveFromCookie(cookie,PRecordData,PRecordTypeInfo,PExpires);
end;
function TMVCSessionWithCookies.CheckAndRetrieveFromCookie(const cookie: RawUTF8;
PRecordData, PRecordTypeInfo: pointer; PExpires: PCardinal): integer;
var clen, len: integer;
now: cardinal;
ccend: PAnsiChar;
cc: TCookieContent;
begin
result := 0; // parsing error
if cookie='' then
exit;
clen := length(cookie);
len := Base64uriToBinLength(clen);
if (len>=sizeof(cc.head)) and (len<=sizeof(cc)) and
Base64uriDecode(pointer(cookie),@cc,clen) then begin
Crypt(@cc,len);
if (cardinal(cc.head.session)<=cardinal(fContext.SessionSequence)) then begin
if PExpires<>nil then
PExpires^ := cc.head.expires;
now := UnixTimeUTC;
if (cc.head.issued<=now) and (cc.head.expires>=now) and
(fContext.Secret.Compute(@cc.head.session,len-8)=cc.head.hmac) then
if PRecordData=nil then
result := cc.head.session else
if (PRecordTypeInfo<>nil) and (len>sizeof(cc.head)) then begin
ccend := PAnsiChar(@cc)+len;
if RecordLoad(PRecordData^,@cc.data,PRecordTypeInfo,nil,ccend)=ccend then
result := cc.head.session;
end;
end;
end;
if result=0 then
Finalize; // delete any invalid/expired cookie on server side
end;
function TMVCSessionWithCookies.Initialize(PRecordData: pointer;
PRecordTypeInfo: pointer; SessionTimeOutMinutes: cardinal): integer;
var len: integer;
cc: TCookieContent;
begin
if (PRecordData<>nil) and (PRecordTypeInfo<>nil) then
len := RecordSaveLength(PRecordData^,PRecordTypeInfo) else
len := 0;
if len>sizeof(cc.data) then // all cookies storage should be < 4K
raise EMVCApplication.CreateGotoError('Big Fat Cookie');
result := InterlockedIncrement(fContext.SessionSequence);
if result=MaxInt-1024 then
fContext.SessionSequence := 0; // thread-safe overflow rounding
cc.head.cryptnonce := Random32gsl;
cc.head.session := result;
cc.head.issued := UnixTimeUTC;
if SessionTimeOutMinutes=0 then
SessionTimeOutMinutes := 31*24*60; // 1 month is a reasonable high value
cc.head.expires := cc.head.issued+SessionTimeOutMinutes*60;
if len>0 then
RecordSave(PRecordData^,@cc.data,PRecordTypeInfo);
inc(len,sizeof(cc.head));
cc.head.hmac := fContext.Secret.Compute(@cc.head.session,len-8);
Crypt(@cc,len);
SetCookie(BinToBase64URI(@cc,len));
end;
procedure TMVCSessionWithCookies.Finalize;
begin
SetCookie(COOKIE_EXPIRED);
end;
function TMVCSessionWithCookies.LoadContext(const Saved: RawUTF8): boolean;
begin
result := RecordLoadBase64(pointer(Saved),length(Saved),
fContext,TypeInfo(TMVCSessionWithCookiesContext));
end;
function TMVCSessionWithCookies.SaveContext: RawUTF8;
begin
result := RecordSaveBase64(fContext,TypeInfo(TMVCSessionWithCookiesContext));
end;
{ TMVCSessionWithRestServer }
function TMVCSessionWithRestServer.GetCookie: RawUTF8;
begin
result := ServiceContext.Request.InCookie[fContext.CookieName];
end;
procedure TMVCSessionWithRestServer.SetCookie(const cookie: RawUTF8);
var ctxt: TSQLRestServerURIContext;
begin
ctxt := ServiceContext.Request;
ctxt.OutSetCookie := fContext.CookieName+'='+cookie;
ctxt.InCookie[CookieName] := cookie;
end;
{ TMVCSessionSingle }
function TMVCSessionSingle.GetCookie: RawUTF8;
begin
result := fSingleCookie;
end;
procedure TMVCSessionSingle.SetCookie(const cookie: RawUTF8);
begin
fSingleCookie := cookie;
end;
{ EMVCApplication }
constructor EMVCApplication.CreateDefault(aStatus: cardinal);
begin
inherited CreateFmt('CreateDefault(%d)',[aStatus]);
TMVCApplication.GotoDefault(fAction,aStatus);
end;
constructor EMVCApplication.CreateGotoError(const aErrorMessage: string;
aErrorCode: integer);
begin
inherited CreateFmt('Error #%d: %s',[aErrorCode,aErrorMessage]);
TMVCApplication.GotoError(fAction,aErrorMessage,aErrorCode);
end;
constructor EMVCApplication.CreateGotoError(aHtmlErrorCode: integer);
begin
inherited CreateFmt('Error=%d',[aHtmlErrorCode]);
TMVCApplication.GotoError(fAction,aHtmlErrorCode);
end;
constructor EMVCApplication.CreateGotoView(const aMethod: RawUTF8;
const aParametersNameValuePairs: array of const; aStatus: cardinal);
begin
inherited CreateFmt('GotoView(''%s'',%d)',[aMethod,aStatus]);
TMVCApplication.GotoView(fAction,aMethod,aParametersNameValuePairs,aStatus);
end;
{ TMVCApplication }
procedure TMVCApplication.Start(aRestModel: TSQLRest; aInterface: PTypeInfo);
var m: integer;
entry: PInterfaceEntry;
begin
fLocker := TAutoLocker.Create;
fRestModel := aRestModel;
fFactory := TInterfaceFactory.Get(aInterface);
fFactoryErrorIndex := fFactory.FindMethodIndex('Error');
if fFactoryErrorIndex<0 then
raise EMVCException.CreateUTF8(
'% does not implement the IMVCApplication.Error() method',[aInterface.Name]);
entry := GetInterfaceEntry(fFactory.InterfaceIID);
if entry=nil then
raise EMVCException.CreateUTF8('%.Start(%): this class should implement %',
[self,aRestModel,fFactory.InterfaceTypeInfo^.Name]);
fFactoryEntry := PAnsiChar(self)+entry^.IOffset;
for m := 0 to fFactory.MethodsCount-1 do
if not MethodHasView(fFactory.Methods[m]) then
with fFactory.Methods[m] do
if ArgsOutFirst<>ArgsResultIndex then
raise EMVCException.CreateUTF8(
'%.Start(%): %.% var/out parameters not allowed with TMVCAction result',
[self,aRestModel,fFactory.InterfaceTypeInfo^.Name,URI]) else
// TServiceCustomAnswer maps TMVCAction in TMVCApplication.RunOnRestServer
ArgsResultIsServiceCustomAnswer := true;
FlushAnyCache;
end;
destructor TMVCApplication.Destroy;
begin
inherited;
fMainRunner.Free;
fSession.Free;
end;
procedure TMVCApplication.Error(var Msg: RawUTF8; var Scope: variant);
begin // do nothing: just pass input error Msg and data Scope to the view
end;
class procedure TMVCApplication.GotoView(var Action: TMVCAction; const MethodName: RawUTF8;
const ParametersNameValuePairs: array of const; status: cardinal);
begin
Action.ReturnedStatus := status;
Action.RedirectToMethodName := MethodName;
if high(ParametersNameValuePairs)<1 then
Action.RedirectToMethodParameters := '' else
Action.RedirectToMethodParameters := JSONEncode(ParametersNameValuePairs);
end;
class procedure TMVCApplication.GotoError(var Action: TMVCAction;
const Msg: string; ErrorCode: integer);
begin
GotoView(Action,'Error',['Msg',Msg],ErrorCode);
end;
class procedure TMVCApplication.GotoError(var Action: TMVCAction;
ErrorCode: integer);
begin
if ErrorCode<=HTTP_CONTINUE then
ErrorCode := HTTP_BADREQUEST;
GotoView(Action,'Error',['Msg',StatusCodeToErrorMsg(ErrorCode)],ErrorCode);
end;
class procedure TMVCApplication.GotoDefault(var Action: TMVCAction; Status: cardinal);
begin
Action.ReturnedStatus := Status;
Action.RedirectToMethodName := 'Default';
Action.RedirectToMethodParameters := '';
end;
procedure TMVCApplication.SetSession(Value: TMVCSessionAbstract);
begin
FreeAndNil(fSession);
fSession := Value;
end;
procedure TMVCApplication.GetViewInfo(MethodIndex: integer; out info: variant);
begin
if MethodIndex>=0 then
info := _ObjFast(['pageName',fFactory.Methods[MethodIndex].URI]) else
info := _ObjFast([]);
end;
procedure TMVCApplication.GetMvcInfo(out info: variant);
begin
info := _ObjFast(['name',fFactory.InterfaceTypeInfo^.Name,
'mORMot',SYNOPSE_FRAMEWORK_VERSION,'root',RestModel.Model.Root,
'methods',ContextFromMethods(fFactory)]);
end;
procedure TMVCApplication.FlushAnyCache;
begin
if fMainRunner<>nil then
fMainRunner.NotifyContentChanged;
end;
{ TMVCRendererAbstract }
constructor TMVCRendererAbstract.Create(aApplication: TMVCApplication);
begin
fApplication := aApplication;
end;
procedure TMVCRendererAbstract.CommandError(const ErrorName: RawUTF8;
const ErrorValue: variant; ErrorCode: Integer);
var info, renderContext: variant;
begin
fApplication.GetViewInfo(fMethodIndex,info);
renderContext := _ObjFast(['main',info, 'msg',StatusCodeToErrorMsg(ErrorCode),
'errorCode',ErrorCode, ErrorName,ErrorValue]);
renderContext.originalErrorContext := JSONReformat(ToUTF8(renderContext));
Renders(renderContext,ErrorCode,true);
end;
procedure TMVCRendererAbstract.ExecuteCommand(aMethodIndex: integer);
var action: TMVCAction;
exec: TServiceMethodExecute;
isAction: boolean;
WR: TTextWriter;
methodOutput: RawUTF8;
renderContext, info: variant;
err: shortstring;
tmp: TTextWriterStackBuffer;
begin
action.ReturnedStatus := HTTP_SUCCESS;
fMethodIndex := aMethodIndex;
try
if fMethodIndex>=0 then begin
repeat
try
isAction := fApplication.fFactory.Methods[fMethodIndex].ArgsResultIsServiceCustomAnswer;
WR := TJSONSerializer.CreateOwnedStream(tmp);
try
WR.Add('{');
exec := TServiceMethodExecute.Create(@fApplication.fFactory.Methods[fMethodIndex]);
try
exec.Options := [optVariantCopiedByReference];
exec.ServiceCustomAnswerStatus := action.ReturnedStatus;
err := '';
if not exec.ExecuteJson([fApplication.fFactoryEntry],pointer(fInput),WR,@err,true) then
if err<>'' then
raise EMVCException.CreateUTF8('%.CommandRunMethod: %',[self,err]) else
with fApplication.fFactory do
raise EMVCException.CreateUTF8('%.CommandRunMethod: %.%() execution error',
[self,InterfaceTypeInfo^.Name,Methods[fMethodIndex].URI]);
action.RedirectToMethodName := exec.ServiceCustomAnswerHead;
action.ReturnedStatus := exec.ServiceCustomAnswerStatus;
finally
exec.Free;
end;
if not isAction then
WR.Add('}');
WR.SetText(methodOutput);
finally
WR.Free;
end;
if isAction then
// was a TMVCAction mapped in a TServiceCustomAnswer record
action.RedirectToMethodParameters := methodOutput else begin
// rendering, e.g. with fast Mustache {{template}}
_Json(methodOutput,renderContext,JSON_OPTIONS_FAST);
fApplication.GetViewInfo(fMethodIndex,info);
_Safe(renderContext)^.AddValue('main',info);
if fMethodIndex=fApplication.fFactoryErrorIndex then
_ObjAddProps(['errorCode',action.ReturnedStatus,
'originalErrorContext',JSONReformat(ToUTF8(renderContext))],
renderContext);
Renders(renderContext,action.ReturnedStatus,false);
exit; // success
end;
except
on E: EMVCApplication do
action := E.fAction;
end; // lower level exceptions will be handled below
fInput := action.RedirectToMethodParameters;
fMethodIndex := fApplication.fFactory.FindMethodIndex(action.RedirectToMethodName);
if action.ReturnedStatus=0 then
action.ReturnedStatus := HTTP_SUCCESS else
if (action.ReturnedStatus=HTTP_TEMPORARYREDIRECT) or
(action.ReturnedStatus=HTTP_FOUND) or
(action.ReturnedStatus=HTTP_SEEOTHER) or
(action.ReturnedStatus=HTTP_MOVEDPERMANENTLY) then
if Redirects(action) then // if redirection is implemented
exit else
action.ReturnedStatus := HTTP_SUCCESS; // fallback is to handle here
until fMethodIndex<0; // loop to handle redirection
end;
// if we reached here, there was a wrong URI -> render the 404 error page
CommandError('notfound',true,HTTP_NOTFOUND);
except
on E: Exception do
CommandError('exception',
ObjectToVariantDebug(E,'%.ExecuteCommand',[self]),HTTP_SERVERERROR);
end;
end;
function TMVCRendererAbstract.Redirects(const action: TMVCAction): boolean;
begin
result := false;
end; // indicates redirection did not happen -> caller should do it manually
{ TMVCRendererFromViews }
constructor TMVCRendererFromViews.Create(aRun: TMVCRunWithViews);
begin
inherited Create(aRun);
fCacheEnabled := true;
end;
procedure TMVCRendererFromViews.Renders(var outContext: variant;
status: cardinal; forcesError: boolean);
var view: TMVCView;
begin
view.Flags := fRun.fViews.fViewFlags;
if forcesError or (fMethodIndex=fRun.fViews.fFactoryErrorIndex) then
try // last change rendering of the error page
fRun.fViews.Render(fRun.fViews.fFactoryErrorIndex,outContext,view);
except // fallback to default HTML error template, if current did not work
on E: Exception do begin
_ObjAddProps(['exceptionName',E.ClassName,
'exceptionMessage',E.Message,'className',ClassName],outContext);
view.Content := TSynMustache.Parse(MUSTACHE_DEFAULTERROR).Render(outContext);
view.ContentType := HTML_CONTENT_TYPE;
end;
end else
fRun.fViews.Render(fMethodIndex,outContext,view);
fOutput.Content := view.Content;
fOutput.Header := HEADER_CONTENT_TYPE+view.ContentType;
fOutput.Status := status;
fOutputFlags := view.Flags;
end;
{ TMVCRendererJson }
procedure TMVCRendererJson.Renders(var outContext: variant;
status: cardinal; forcesError: boolean);
begin
fOutput.Content := JSONReformat(ToUTF8(outContext));
fOutput.Header := JSON_CONTENT_TYPE_HEADER_VAR;
fOutput.Status := status;
end;
{ TMVCRun }
constructor TMVCRun.Create(aApplication: TMVCApplication);
begin
fApplication := aApplication;
fApplication.SetSession(nil);
end;
procedure TMVCRun.NotifyContentChangedForMethod(aMethodIndex: integer);
begin // do nothing at this abstract level
end;
procedure TMVCRun.NotifyContentChanged;
var m: integer;
begin
for m := 0 to fApplication.fFactory.MethodsCount-1 do
NotifyContentChangedForMethod(m)
end;
procedure TMVCRun.NotifyContentChangedForMethod(const aMethodName: RawUTF8);
begin
NotifyContentChangedForMethod(fApplication.fFactory.FindMethodIndex(aMethodName));
end;
{ TMVCRunWithViews }
constructor TMVCRunWithViews.Create(aApplication: TMVCApplication;
aViews: TMVCViewsAbstract);
begin
inherited Create(aApplication);
fViews := aViews;
fCacheLocker := TAutoLocker.Create;
end;
function TMVCRunWithViews.SetCache(const aMethodName: RawUTF8;
aPolicy: TMVCRendererCachePolicy; aTimeOutSeconds: cardinal): TMVCRunWithViews;
const MAX_CACHE_TIMEOUT = 60*15; // 15 minutes
var aMethodIndex: integer;
begin
with fCacheLocker.ProtectMethod do begin
aMethodIndex := fApplication.fFactory.CheckMethodIndex(aMethodName);
if fCache=nil then
SetLength(fCache,fApplication.fFactory.MethodsCount);
with fCache[aMethodIndex] do begin
Policy := aPolicy;
if aTimeOutSeconds-1>=MAX_CACHE_TIMEOUT then
TimeOutSeconds := MAX_CACHE_TIMEOUT else
TimeOutSeconds := aTimeOutSeconds;
NotifyContentChangedForMethod(aMethodIndex);
end;
end;
result := self;
end;
destructor TMVCRunWithViews.Destroy;
begin
fViews.Free;
inherited;
end;
procedure TMVCRunWithViews.NotifyContentChangedForMethod(aMethodIndex: integer);
begin
inherited;
with fCacheLocker.ProtectMethod do
if cardinal(aMethodIndex)'' then
fRestServer.ServiceMethodRegister(aSubURI,RunOnRestServerSub,bypass) else begin
for m := 0 to fApplication.fFactory.MethodsCount-1 do begin
method := fApplication.fFactory.Methods[m].URI;
if method[1]='_' then
delete(method,1,1); // e.g. IService._Start() -> /service/start
fRestServer.ServiceMethodRegister(method,RunOnRestServerRoot,bypass);
end;
if publishMvcInfo in fPublishOptions then
fRestServer.ServiceMethodRegister(MVCINFO_URI,RunOnRestServerRoot,bypass);
if publishStatic in fPublishOptions then
fRestServer.ServiceMethodRegister(STATIC_URI,RunOnRestServerRoot,bypass);
end;
if (registerORMTableAsExpressions in fPublishOptions) and
aViews.InheritsFrom(TMVCViewsMustache) then
TMVCViewsMustache(aViews).RegisterExpressionHelpersForTables(fRestServer);
fStaticCache.Init({casesensitive=}true);
fApplication.SetSession(TMVCSessionWithRestServer.Create);
end;
function TMVCRunOnRestServer.AddStaticCache(const aFileName: TFileName;
const aFileContent: RawByteString): RawByteString;
begin
if aFileContent<>'' then // also cache content-type
result := GetMimeContentType(
pointer(aFileContent),length(aFileContent),aFileName)+#10+aFileContent else
result := '';
fStaticCache.Add(StringToUTF8(aFileName),result);
end;
procedure TMVCRunOnRestServer.InternalRunOnRestServer(
Ctxt: TSQLRestServerURIContext; const MethodName: RawUTF8);
var mvcinfo, inputContext: variant;
rawMethodName,rawFormat,static,body,content: RawUTF8;
staticFileName: TFileName;
rendererClass: TMVCRendererReturningDataClass;
renderer: TMVCRendererReturningData;
methodIndex: integer;
method: PServiceMethod;
timer: TPrecisionTimer;
begin
Split(MethodName,'/',rawMethodName,rawFormat);
// 1. implement mvc-info endpoint
if (publishMvcInfo in fPublishOptions) and
IdemPropNameU(rawMethodName,MVCINFO_URI) then begin
if fMvcInfoCache='' then begin
fApplication.GetMvcInfo(mvcinfo);
mvcinfo.viewsFolder := fViews.ViewTemplateFolder;
fMvcInfoCache := TSynMustache.Parse(MUSTACHE_MVCINFO).Render(mvcinfo);
end;
Ctxt.Returns(fMvcInfoCache,HTTP_SUCCESS,HTML_CONTENT_TYPE_HEADER,True);
end else
// 2. serve static resources, with proper caching
if (publishStatic in fPublishOptions) and
IdemPropNameU(rawMethodName,STATIC_URI) then begin
// code below will use a local in-memory cache, but would do the same as:
// Ctxt.ReturnFileFromFolder(fViews.ViewStaticFolder);
fCacheLocker.Enter;
try
if cacheStatic in fPublishOptions then
static := fStaticCache.Value(rawFormat,#0) else
static := #0;
if static=#0 then // static='' means HTTP_NOTFOUND
if PosEx('..',rawFormat)>0 then // avoid injection
static := '' else begin
staticFileName := UTF8ToString(StringReplaceChars(rawFormat,'/',PathDelim));
if cacheStatic in fPublishOptions then begin // retrieve and cache
static := fViews.GetStaticFile(staticFileName);
static := AddStaticCache(staticFileName,static);
end else begin // no cache
staticFileName := fViews.ViewStaticFolder + staticFileName;
Ctxt.ReturnFile(staticFileName,{handle304=}true,'','','',fStaticCacheControlMaxAge);
exit;
end;
end;
finally
fCacheLocker.Leave;
end;
if static='' then
Ctxt.Error('',HTTP_NOTFOUND,fStaticCacheControlMaxAge) else begin
Split(static,#10,content,static);
Ctxt.Returns(static,HTTP_SUCCESS,HEADER_CONTENT_TYPE+content,
{handle304=}true,false,fStaticCacheControlMaxAge);
end;
end else begin
// 3. render regular page using proper viewer
timer.Start;
if IdemPropNameU(rawFormat,'json') then
rendererClass := TMVCRendererJSON else
rendererClass := TMVCRendererFromViews;
renderer := rendererClass.Create(self);
try
if Ctxt.Method in [mGET,mPOST] then begin
methodIndex := fApplication.fFactory.FindMethodIndex(rawMethodName);
if methodIndex>=0 then begin
method := @fApplication.fFactory.Methods[methodIndex];
inputContext := Ctxt.GetInputAsTDocVariant(JSON_OPTIONS_FAST_EXTENDED,method);
if not VarIsEmpty(inputContext) then
with _Safe(inputContext)^ do begin
if (Kind=dvObject) and (Count>0) then
// try {"p.a1":5,"p.a2":"dfasdfa"} -> {"p":{"a1":5,"a2":"dfasdfa"}}
if method^.ArgsInputValuesCount=1 then
FlattenAsNestedObject(RawUTF8(method^.Args[method^.ArgsInFirst].ParamName^));
renderer.fInput := ToJSON;
end;
end;
renderer.ExecuteCommand(methodIndex);
end else
renderer.CommandError('notfound',true,HTTP_NOTFOUND);
body := renderer.Output.Content;
if viewHasGenerationTimeTag in renderer.fOutputFlags then
body := StringReplaceAll(body,fViews.ViewGenerationTimeTag,
ShortStringToAnsi7String(timer.Stop));
Ctxt.Returns(body,renderer.Output.Status,
renderer.Output.Header,{handle304=}true,{noerrorprocess=}true,
{cachecontrol=}0,{hashwithouttime:}crc32cUTF8ToHex(renderer.Output.Content));
finally
renderer.Free;
end;
end;
end;
procedure TMVCRunOnRestServer.RunOnRestServerRoot(Ctxt: TSQLRestServerURIContext);
begin
InternalRunOnRestServer(Ctxt,Ctxt.URI+'/'+Ctxt.URIBlobFieldName);
end;
procedure TMVCRunOnRestServer.RunOnRestServerSub(Ctxt: TSQLRestServerURIContext);
begin
if Ctxt.URIBlobFieldName='' then
Ctxt.Redirect(Ctxt.URIWithoutSignature+'/default') else
InternalRunOnRestServer(Ctxt,Ctxt.URIBlobFieldName);
end;
{ TMVCRendererReturningData }
constructor TMVCRendererReturningData.Create(aRun: TMVCRunWithViews);
begin
fRun := aRun;
inherited Create(fRun.Application);
end;
procedure TMVCRendererReturningData.ExecuteCommand(aMethodIndex: integer);
procedure SetOutputValue(const aValue: RawUTF8);
begin
fOutput.Status := HTTP_SUCCESS;
Split(aValue,#0,fOutput.Header,RawUTF8(fOutput.Content));
end;
function RetrievedFromInputValues(const aKey: RawUTF8;
const aInputValues: TSynNameValue): boolean;
var i: integer;
begin
i := aInputValues.Find(aKey);
if (i>=0) and (aInputValues.List[i].Value<>'') and
(fCacheCurrentSec'') and (fCacheCurrentSecnoCache then
try
fRun.fCacheLocker.Enter;
with fRun.fCache[aMethodIndex] do begin
inc(fCacheCurrentSec,TimeOutSeconds);
case fCacheCurrent of
rootCache:
if fOutput.Status=HTTP_SUCCESS then begin
RootValue := fOutput.Header+#0+fOutput.Content;
RootValueExpirationTime := fCacheCurrentSec;
end else
RootValue := '';
inputCache:
if fOutput.Status=HTTP_SUCCESS then
InputValues.Add(fCacheCurrentInputValueKey,fOutput.Header+#0+fOutput.Content,fCacheCurrentSec) else
InputValues.Add(fCacheCurrentInputValueKey,'');
end;
end;
finally
fRun.fCacheLocker.Leave;
end;
end;
function TMVCRendererReturningData.Redirects(const action: TMVCAction): boolean;
begin
fOutput.Header := 'Location: '+UrlEncodeJsonObject(action.RedirectToMethodName,
action.RedirectToMethodParameters,['main']);
fOutput.Status := action.ReturnedStatus;
result := true;
end;
initialization
assert(sizeof(TMVCAction)=sizeof(TServiceCustomAnswer));
end.
|