support for delphi 11.1

This commit is contained in:
Razor12911
2022-05-13 13:05:10 +02:00
parent 8ceccef928
commit 39fb5ae479
167 changed files with 8914 additions and 3205 deletions

View File

@@ -19,12 +19,14 @@ type
fTitle: RawUTF8;
fLanguage: RawUTF8;
fAbout: RawUTF8;
fLink: RawUTF8;
published
property Title: RawUTF8 index 80 read fTitle write fTitle;
property Language: RawUTF8 index 3 read fLanguage write fLanguage;
property Description: RawUTF8 index 120 read fDescription write fDescription;
property Copyright: RawUTF8 index 80 read fCopyright write fCopyright;
property About: RawUTF8 read fAbout write fAbout;
property Link: RawUTF8 index 60 read fLink write fLink;
end;
TSQLRecordTimeStamped = class(TSQLRecord)
@@ -46,6 +48,7 @@ type
fHashedPassword: RawUTF8;
fLogonName: RawUTF8;
public
function ComputeHash(const PlainPassword: RawUTF8): RawUTF8; virtual;
procedure SetPlainPassword(const PlainPassword: RawUTF8);
function CheckPlainPassword(const PlainPassword: RawUTF8): boolean;
function Name: RawUTF8;
@@ -96,6 +99,7 @@ type
OrderID: TIntegerDynArray;
procedure Init(aRest: TSQLRest);
function Get(tagID: integer): RawUTF8;
function GetIDFromIdent(const Ident: RawUTF8): integer;
procedure SaveOccurence(aRest: TSQLRest);
procedure SortTagsByIdent(var Tags: TIntegerDynArray);
function GetAsDocVariantArray: Variant;
@@ -106,6 +110,7 @@ type
fAbstract: RawUTF8;
fPublishedMonth: Integer;
fTags: TIntegerDynArray;
fLegacyHash: Int64;
public
class function CurrentPublishedMonth: Integer;
class procedure InitializeTable(Server: TSQLRestServer; const FieldName: RawUTF8;
@@ -118,6 +123,8 @@ type
property Abstract: RawUTF8 read fAbstract write fAbstract;
// "index 1" below to allow writing e.g. aArticle.DynArray(1).Delete(aIndex)
property Tags: TIntegerDynArray index 1 read fTags write fTags;
// xxhash32 of legacy post_url
property LegacyHash: Int64 read fLegacyHash write fLegacyHash;
end;
TSQLArticleSearch = class(TSQLRecordFTS4Porter)
@@ -157,6 +164,8 @@ procedure DotClearFlatImport(Rest: TSQLRest; const aFlatFile: RawUTF8;
var aTagsLookup: TSQLTags; const aDotClearRoot: RawUTF8;
const aStaticFolder: TFileName);
function ComputeLegacyHash(url: PUTF8Char): cardinal;
implementation
@@ -176,12 +185,16 @@ end;
{ TSQLSomeone }
const
SALT = 'mORMot';
function TSQLSomeone.ComputeHash(const PlainPassword: RawUTF8): RawUTF8;
var dig: THash256;
begin
PBKDF2_SHA3(SHA3_224,PlainPassword,LogonName+'@mORMot',30,@dig);
BinToHexLower(@dig,28,result);
end;
function TSQLSomeone.CheckPlainPassword(const PlainPassword: RawUTF8): boolean;
begin
result := fHashedPassword=SHA256(SALT+LogonName+PlainPassword);
result := fHashedPassword=ComputeHash(PlainPassword);
end;
function TSQLSomeone.Name: RawUTF8;
@@ -191,7 +204,7 @@ end;
procedure TSQLSomeone.SetPlainPassword(const PlainPassword: RawUTF8);
begin
fHashedPassword := SHA256(SALT+LogonName+PlainPassword);
fHashedPassword := ComputeHash(PlainPassword);
end;
@@ -233,6 +246,8 @@ begin
inherited;
if (FieldName='') or (FieldName='PublishedMonth') then
Server.CreateSQLIndex(TSQLArticle,'PublishedMonth',false);
if (FieldName='') or (FieldName='LegacyHash') then
Server.CreateSQLIndex(TSQLArticle,'LegacyHash',false);
end;
procedure TSQLArticle.SetPublishedMonth(FromTime: TTimeLog);
@@ -260,8 +275,20 @@ begin
result := '';
end;
function TSQLTags.GetIDFromIdent(const Ident: RawUTF8): integer;
var i: PtrInt;
begin
if Ident<>'' then
for i := 0 to length(Lookup)-1 do
if IdemPropNameU(Lookup[i].Ident,Ident) then begin
result := i+1;
exit;
end;
result := 0;
end;
function TSQLTags.GetAsDocVariantArray: Variant;
var i,ndx: Integer;
var i,ndx: PtrInt;
begin
TDocVariant.NewFast(result);
with Lock.ProtectMethod do
@@ -438,21 +465,46 @@ begin
until P=nil;
end;
function HttpGet(const aURI: SockString; outHeaders: PSockString=nil;
forceNotSocket: boolean=false; outStatus: PInteger=nil): SockString;
begin
result := '';
if outStatus<>nil then
outStatus^ := 404;
end;
function ComputeLegacyHash(url: PUTF8Char): cardinal;
var c: ansichar;
begin
result := 0;
if url<>nil then
repeat
case url^ of
#0: exit;
'a'..'z', 'A'..'Z', '0'..'9': begin
c := upcase(url^);
result := crc32c(result, @c, 1);
end;
end;
inc(url);
until false;
end;
procedure DotClearFlatImport(Rest: TSQLRest; const aFlatFile: RawUTF8;
var aTagsLookup: TSQLTags; const aDotClearRoot: RawUTF8;
const aStaticFolder: TFileName);
var T,tagTable,postTable: TDotClearTable;
data,urls: TRawUTF8List;
data: TRawUTF8List;
urls: TIntegerDynArray;
info: TSQLBlogInfo;
article: TSQLArticle;
comment: TSQLComment;
tag: TSQLTag;
tags: TRawUTF8DynArray;
tags, notfound: TRawUTF8DynArray;
tagID: TIDDynArray;
tagsCount: integer;
batch: TSQLRestBatch;
PublicFolder: TFileName;
notfound: TRawUTF8DynArray;
r,ndx,post_url,meta_id,meta_type,tag_post_id,postID,post_id: integer;
function FixLinks(P: PUTF8Char): RawUTF8;
@@ -501,13 +553,20 @@ var T,tagTable,postTable: TDotClearTable;
continue;
AddNoJSONEscape(B,H-B);
P := H;
if IdemPChar(P,'HTTP://BLOG.SYNOPSE.INFO/') then
inc(P,24)
else if IdemPChar(P,'HTTPS://BLOG.SYNOPSE.INFO/') then
inc(P,25);
if IdemPChar(P,'HTTP://SYNOPSE.INFO') then begin
AddShort('https://synopse.info');
inc(P,19);
end else if P^='/' then begin
if P[1]='?' then
inc(P);
if IdemPChar(P+1,'POST/') then begin
GetUrl(P+6);
i := urls.IndexOf(urlnoparam);
i := IntegerScanIndex(pointer(urls),length(urls),
ComputeLegacyHash(pointer(urlnoparam)));
if i>=0 then begin
AddShort('articleView?id=');
Add(i+1);
@@ -582,7 +641,6 @@ begin
end;
auto1 := TAutoFree.Several([
@data,TDotClearTable.Parse(aFlatFile),
@urls,TRawUTF8ListHashed.Create,
@batch,TSQLRestBatch.Create(Rest,TSQLTag,5000)]);
auto2 := TSQLRecord.AutoFree([ // avoid several try..finally
@info,TSQLBlogInfo, @article,TSQLArticle, @comment,TSQLComment, @tag,TSQLTag]);
@@ -614,7 +672,7 @@ begin
post_url := postTable.FieldIndexExisting('post_url');
if postTable.Step(true) then
repeat
urls.Add(postTable.FieldBuffer(post_url));
AddInteger(urls,ComputeLegacyHash(postTable.FieldBuffer(post_url)));
until not postTable.Step;
article.Author := TSQLAuthor(1);
article.AuthorName := 'synopse';
@@ -631,6 +689,7 @@ begin
article.ModifiedAt := Iso8601ToTimeLog(postTable.GetU(r,'post_upddt'));
article.SetPublishedMonth(article.CreatedAt);
postID := postTable.GetAsInteger(r,post_id);
article.LegacyHash := ComputeLegacyHash(postTable.Get(r,post_url));
article.Tags := nil;
if tagTable.Step(true) then
repeat

View File

@@ -38,17 +38,25 @@ var aModel: TSQLModel;
aApplication: TBlogApplication;
aHTTPServer: TSQLHttpServer;
begin
//with TSQLLog.Family do Level := LOG_VERBOSE;
with TSQLLog.Family do begin
Level := LOG_VERBOSE;
PerThreadLog := ptIdentifiedInOnFile;
RotateFileCount := 10;
RotateFileSizeKB := 20 shl 10;
FileExistsAction := acAppend; // as expected by rotation
end;
aModel := CreateModel;
try
aServer := TSQLRestServerDB.Create(aModel,ChangeFileExt(ExeVersion.ProgramFileName,'.db'));
try
aServer.DB.Synchronous := smNormal;
aServer.DB.LockingMode := lmExclusive;
aServer.Options := aServer.Options+[rsoNoTableURI];
aServer.CreateMissingTables;
aApplication := TBlogApplication.Create;
try
aApplication.Start(aServer);
aServer.ServiceMethodRegisterPublishedMethods('', aApplication);
aHTTPServer := TSQLHttpServer.Create('8092',aServer
{$ifndef ONLYUSEHTTPSOCKET},'+',useHttpApiRegisteringURI{$endif});
try

View File

@@ -12,6 +12,7 @@ uses
SynCommons,
SynLog,
SynTests,
SynCrtSock,
mORMot,
mORMotMVC,
MVCModel;
@@ -31,8 +32,9 @@ type
out Comments: TObjectList);
procedure AuthorView(
var ID: TID; out Author: TSQLAuthor; out Articles: variant);
function Login(
const LogonName,PlainPassword: RawUTF8): TMVCAction;
procedure LoginView;
function Login(const LogonName,PlainPassword,
NewPlainPassword1,NewPlainPassword2: RawUTF8): TMVCAction;
function Logout: TMVCAction;
function ArticleComment(ID: TID; const Title,Comment: RawUTF8): TMVCAction;
function ArticleMatch(const Match: RawUTF8): TMVCAction;
@@ -71,7 +73,13 @@ type
procedure TagToText(const Value: variant; out result: variant);
public
procedure Start(aServer: TSQLRestServer); reintroduce;
published
// low-level blog/post blog/tag blog/rss endpoints
procedure Post(Ctxt: TSQLRestServerURIContext);
procedure Tag(Ctxt: TSQLRestServerURIContext);
procedure Rss(Ctxt: TSQLRestServerURIContext);
public
// IBlogApplication implemented methods
procedure Default(var Scope: variant);
procedure ArticleView(ID: TID;
var WithComments: boolean; Direction: integer; var Scope: variant;
@@ -79,7 +87,9 @@ type
out Comments: TObjectList);
procedure AuthorView(
var ID: TID; out Author: TSQLAuthor; out Articles: variant);
function Login(const LogonName,PlainPassword: RawUTF8): TMVCAction;
procedure LoginView;
function Login(const LogonName,PlainPassword,
NewPlainPassword1,NewPlainPassword2: RawUTF8): TMVCAction;
function Logout: TMVCAction;
function ArticleComment(ID: TID; const Title,Comment: RawUTF8): TMVCAction;
function ArticleMatch(const Match: RawUTF8): TMVCAction;
@@ -130,6 +140,10 @@ begin
finally
Free;
end;
_Safe(fBlogMainInfo)^.AddValue('engine',RawUTF8ToVariant(
'Website powered by mORMot MVC '+SYNOPSE_FRAMEWORK_VERSION+
', compiled with '+GetDelphiCompilerVersion+
', running on '+RawUTF8(ToText(OSVersion32))+'.'));
end;
procedure TBlogApplication.MonthToText(const Value: variant;
@@ -172,29 +186,28 @@ begin
auto := TSQLRecord.AutoFree([ // avoid several try..finally
@info,TSQLBlogInfo, @article,TSQLArticle, @comment,TSQLComment, @tag,TSQLTag]);
if not RestModel.Retrieve('',info) then begin // retrieve first item
tmp := StringFromFile('/home/ab/Downloads/2020-06-16-a8003957c2ae6bde5be6ea279c9c9ce4-backup.txt');
tmp := StringFromFile(ExeVersion.ProgramFilePath+'2021-01-20-16-37-default-backup.txt');
info.Language := 'en';
if tmp<>'' then begin
info.Title := 'Synopse Blog';
info.Description := 'Articles, announcements, news, updates and more '+
'about our Open Source projects';
'about Synopse Open Source projects';
info.About := 'Latest information about Synopse Open Source librairies, '+
'mainly the mORMot ORM/SOA/MVC framework, and SynPDF.';
info.Link := 'https://blog.synopse.info';
end else begin
info.Title := 'mORMot BLOG';
info.Description := 'Sample Blog Web Application using Synopse mORMot MVC';
info.About := TSynTestCase.RandomTextParagraph(10,'!');
info.Link := 'http://localhost:8092';
end;
info.About := info.About+#13#10'Website powered by mORMot MVC '+
SYNOPSE_FRAMEWORK_VERSION+', compiled with '+GetDelphiCompilerVersion+
', running on '+ToText(OSVersion32)+'.';
info.Copyright := '&copy;'+ToUTF8(CurrentYear)+'<a href=https://synopse.info>Synopse Informatique</a>';
RestModel.Add(info,true);
end;
if RestModel.TableHasRows(TSQLArticle) then
exit;
if tmp<>'' then begin
DotClearFlatImport(RestModel,tmp,fTagsLookup,'http://blog.synopse.info',
DotClearFlatImport(RestModel,tmp,fTagsLookup,'https://blog.synopse.info',
(TMVCRunOnRestServer(fMainRunner).Views as TMVCViewsMustache).ViewStaticFolder);
exit;
end;
@@ -271,20 +284,24 @@ begin
fDefaultData.AddNewProp('tags',fTagsLookup.GetAsDocVariantArray,info);
end;
procedure TBlogApplication.FlushAnyCache;
begin
inherited FlushAnyCache; // call fMainRunner.NotifyContentChanged
fDefaultData.Clear;
end;
{ TBlogApplication - Commands }
const
ARTICLE_FIELDS = 'RowID,Title,Tags,Abstract,ContentHtml,Author,AuthorName,CreatedAt';
ARTICLE_DEFAULT_LIMIT = ' limit 20';
ARTICLE_DEFAULT_ORDER: RawUTF8 = 'order by RowID desc'+ARTICLE_DEFAULT_LIMIT;
procedure TBlogApplication.FlushAnyCache;
begin
inherited FlushAnyCache; // call fMainRunner.NotifyContentChanged
fDefaultData.Clear;
// get last 20 articles
fDefaultData.SetValue('Articles',
RestModel.RetrieveDocVariantArray(TSQLArticle,'',
ARTICLE_DEFAULT_ORDER,[],ARTICLE_FIELDS,nil,@fDefaultLastID));
end;
{ TBlogApplication - Commands }
procedure TBlogApplication.Default(var Scope: variant);
var scop: PDocVariantData;
lastID: TID;
@@ -320,14 +337,12 @@ begin
whereClause := whereClause+' and IntegerDynArrayContains(Tags,?)';
end;
SetVariantNull(Scope);
if (lastID=0) and (tag=0) then begin // use simple cache if no parameters
if not fDefaultData.AddExistingProp('Articles',Scope) then begin
articles := RestModel.RetrieveDocVariantArray(TSQLArticle,'',
ARTICLE_DEFAULT_ORDER,[],ARTICLE_FIELDS,nil,@fDefaultLastID);
fDefaultData.AddNewProp('Articles',articles,Scope);
end;
if (lastID=0) and (tag=0) then begin
// use simple cache if no parameters
fDefaultData.AddExistingProp('Articles',Scope); // set by FlushAnyCache
lastID := fDefaultLastID;
end else begin // use more complex request using lastID + tag parameters
end else begin
// use more complex request using lastID + tag parameters
articles := RestModel.RetrieveDocVariantArray(TSQLArticle,'',
whereClause+ARTICLE_DEFAULT_ORDER,[lastID,tag],ARTICLE_FIELDS,nil,@lastID);
scope := _ObjFast(['Articles',articles]);
@@ -370,12 +385,31 @@ begin
raise EMVCApplication.CreateGotoError(HTTP_NOTFOUND);
end;
function TBlogApplication.Login(const LogonName, PlainPassword: RawUTF8): TMVCAction;
procedure TBlogApplication.LoginView;
begin
end;
function TBlogApplication.Login(const LogonName, PlainPassword,
NewPlainPassword1, NewPlainPassword2: RawUTF8): TMVCAction;
var Author: TSQLAuthor;
SessionInfo: TCookieData;
newpwd: RawUTF8;
begin
if CurrentSession.CheckAndRetrieve<>0 then begin
GotoError(result,HTTP_BADREQUEST);
if LogonName='' then begin
GotoView(result,'LoginView',[]);
exit;
end;
newpwd := Trim(NewPlainPassword1);
if newpwd<>'' then begin
if (newpwd<>NewPlainPassword2) or
(newpwd=PlainPassword) or
(CurrentSession.CheckAndRetrieve(@SessionInfo,TypeInfo(TCookieData))=0) or
(SessionInfo.AuthorName<>LogonName) then begin
GotoError(result,HTTP_NOTACCEPTABLE);
exit;
end;
end else if CurrentSession.CheckAndRetrieve<>0 then begin
GotoError(result,'Already Logged In',HTTP_BADREQUEST);
exit;
end;
Author := TSQLAuthor.Create(RestModel,'LogonName=?',[LogonName]);
@@ -385,6 +419,10 @@ begin
SessionInfo.AuthorID := Author.ID;
SessionInfo.AuthorRights := Author.Rights;
CurrentSession.Initialize(@SessionInfo,TypeInfo(TCookieData));
if newpwd<>'' then begin
Author.SetPlainPassword(newpwd);
RestModel.Update(Author,'HashedPassword');
end;
GotoDefault(result);
end else
GotoError(result,sErrorInvalidLogin);
@@ -481,6 +519,83 @@ begin
end;
end;
procedure TBlogApplication.Post(Ctxt: TSQLRestServerURIContext);
var hash, id: Int64;
begin
hash := ComputeLegacyHash(pointer(UrlDecode(Ctxt.URIAfterRoot,5,-1)));
id := RestModel.OneFieldValueInt64(TSQLArticle,'ID',
FormatUTF8('LegacyHash=:(%):', [hash]));
Ctxt.Redirect(FormatUTF8('/%/articleview?id=%',[RestModel.Model.Root,id]));
end;
procedure TBlogApplication.Tag(Ctxt: TSQLRestServerURIContext);
var
id: integer;
begin
id := fTagsLookup.GetIDFromIdent(copy(Ctxt.UriAfterRoot, 5, 100));
Ctxt.Redirect(FormatUTF8('/%/default?scope={tag:%}',[RestModel.Model.Root,id]));
end;
function Esc(const Msg: RawUTF8): RawUTF8;
var i: integer;
ins: RawUTF8;
begin
// fast enough for our purpose to compute some RSS cache
result := Msg;
for i := length(Msg) downto 1 do begin
case Msg[i] of
'"': ins := '&quot';
'&': ins := '&amp';
'<': ins := '&lt';
'>': ins := '&gt';
else Continue;
end;
result[i] := ';';
insert(ins,result,i);
end;
end;
procedure TBlogApplication.Rss(Ctxt: TSQLRestServerURIContext);
function ComputeRss: variant;
var xml, lng, link: RawUTF8;
art: integer;
begin
with _Safe(fBlogMainInfo)^ do
begin
link := U['Link'];
if (link<>'') and (link[length(link)]='/') then
SetLength(link,length(link)-1);
lng := U['Language'];
if lng='' then
lng := 'en_US';
FormatUTF8('<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">'+
'<channel><title>%</title>'+
'<link>%</link><description>%</description>'+
'<lastBuildDate>%</lastBuildDate><language>%</language>',
[Esc(U['Title']),link,Esc(U['Description']),
DateTimeToHTTPDate(NowUTC,'+0000'),lng],xml);
end;
with _Safe(fDefaultData.GetValue('Articles'))^ do
for art := 0 to Count-1 do
with _Safe(Values[art])^ do
xml := FormatUTF8('%'#13'<item><title>%</title>'+
'<link>%/articleview?id=%</link><pubDate>%</pubDate><category>blog</category>'+
'<dc:creator>%</dc:creator><description><![CDATA[%]]></description>'+
'<content:encoded><![CDATA[%]]></content:encoded></item>',
[xml,Esc(U['Title']),link,I['ID'],
DateTimeToHTTPDate(TimeLogToDateTime(I['CreatedAt']),'+0000'),
Esc(U['AuthorName']),U['Abstract'],U['Content']]);
RawUTF8ToVariant(xml+'</channel></rss>',result);
end;
var
rss: variant;
begin
if not fDefaultData.ExistsOrLock('rss',rss) then
fDefaultData.ReplaceAndUnlock('rss',ComputeRss,rss);
Ctxt.Returns(ToUTF8(rss),HTTP_SUCCESS,
HEADER_CONTENT_TYPE+'application/rss+xml; charset=UTF-8',{handle304=}true);
end;
initialization
{$ifndef DELPHI2010}
// manual definition mandatory only if Delphi 2010 RTTI is not available

View File

@@ -1,51 +1,51 @@
{{>header}}
{{>masthead}}
<div class="blog-header">
<h1 class="blog-title">{{article.title}}</h1>
<div class="lead blog-description">
Written by <a href="authorView?id={{article.Author}}">{{article.AuthorName}}</a> ({{author.FirstName}} {{author.FamilyName}}) on {{TimeLogToText article.CreatedAt}}<br />
{{#article.tags}}<a href="default?scope={tag:{{.}}}" class="label label-info">{{TagToText .}}</a> {{/article.tags}}
</div>
</div>
<div class="row">
<div class="col-sm-8 blog-main">
{{#article}}
{{#ContentHtml}}{{{abstract}}}{{/ContentHtml}}{{^ContentHtml}}{{{WikiToHtml abstract}}}{{/ContentHtml}}
<hr>
{{#ContentHtml}}{{{content}}}{{/ContentHtml}}{{^ContentHtml}}{{{WikiToHtml content}}}{{/ContentHtml}}
{{/article}}
<hr>
<ul class="pager">
<li class="previous"><a href="ArticleView?id={{Article.ID}}&withComments={{withComments}}&direction=1">&larr; Previous</a></li>
<li class="next"><a href="ArticleView?id={{Article.ID}}&withComments={{withComments}}&direction=2">Next &rarr;</a></li>
</ul>
<a name="comments"></a>
{{#WithComments}}
{{#Comments}}
<blockquote>
<p><strong>{{Title}}</strong></p>
<p>{{#ContentHtml}}{{{Content}}}{{/ContentHtml}}{{^ContentHtml}}{{{WikiToHtml Content}}}{{/ContentHtml}}</p>
<footer>Commented on {{TimeLogToText CreatedAt}} by <a href="authorView?id={{Author}}">{{AuthorName}}</a></<footer>
</blockquote>
<hr>
{{/Comments}}
{{^Comments}}<blockquote>No comment yet.</blockquote>{{/Comments}}
<p><a href="ArticleView?id={{Article.ID}}#comments" class="btn btn-primary btn-sm">Hide Comments</a></p>
{{#main.session.AuthorRights.Comment}}
<a name="addComment"></a>
<form class="form-horizontal" action="ArticleComment#addComment" method="post">
<div class="form-group">
{{#Scope}}<div class="alert alert-danger">{{CommentError}}</div>{{/Scope}}
<input type="hidden" name="id" value={{Article.ID}}>
<input type="text" class="form-control" name="title" placeholder="Title" value="{{Scope.CommentTitle}}">
<textarea class="form-control" name="comment" rows="7" placeholder="Enter a new comment here">{{Scope.CommentContent}}</textarea>
<button type="submit" class="btn btn-primary btn-sm">Add Comment</a>
</div>
</form>
{{/main.session.AuthorRights.Comment}}
</p>
{{/WithComments}}
{{^WithComments}}
<p><a href="ArticleView?id={{Article.ID}}&withComments=true#comments" class="btn btn-primary btn-sm">Show Comments</a></p>
{{/WithComments}}
<div class="blog-header">
<h1 class="blog-title">{{article.title}}</h1>
<div class="lead blog-description">
Written by <a href="authorView?id={{article.Author}}">{{article.AuthorName}}</a> ({{author.FirstName}} {{author.FamilyName}}) on {{TimeLogToText article.CreatedAt}}<br />
{{#article.tags}}<a href="default?scope={tag:{{.}}}" class="label label-info">{{TagToText .}}</a> {{/article.tags}}
</div>
</div>
<div class="row">
<div class="col-sm-8 blog-main">
{{#article}}
{{#ContentHtml}}{{{abstract}}}{{/ContentHtml}}{{^ContentHtml}}{{{WikiToHtml abstract}}}{{/ContentHtml}}
<hr>
{{#ContentHtml}}{{{content}}}{{/ContentHtml}}{{^ContentHtml}}{{{WikiToHtml content}}}{{/ContentHtml}}
{{/article}}
<hr>
<ul class="pager">
<li class="previous"><a href="ArticleView?id={{Article.ID}}&withComments={{withComments}}&direction=1">&larr; Previous</a></li>
<li class="next"><a href="ArticleView?id={{Article.ID}}&withComments={{withComments}}&direction=2">Next &rarr;</a></li>
</ul>
<a name="comments"></a>
{{#WithComments}}
{{#Comments}}
<blockquote>
<p><strong>{{Title}}</strong></p>
<p>{{#ContentHtml}}{{{Content}}}{{/ContentHtml}}{{^ContentHtml}}{{{WikiToHtml Content}}}{{/ContentHtml}}</p>
<footer>Commented on {{TimeLogToText CreatedAt}} by <a href="authorView?id={{Author}}">{{AuthorName}}</a></<footer>
</blockquote>
<hr>
{{/Comments}}
{{^Comments}}<blockquote>No comment yet.</blockquote>{{/Comments}}
<p><a href="ArticleView?id={{Article.ID}}#comments" class="btn btn-primary btn-sm">Hide Comments</a></p>
{{#main.session.AuthorRights.Comment}}
<a name="addComment"></a>
<form class="form-horizontal" action="ArticleComment#addComment" method="post">
<div class="form-group">
{{#Scope}}<div class="alert alert-danger">{{CommentError}}</div>{{/Scope}}
<input type="hidden" name="id" value={{Article.ID}}>
<input type="text" class="form-control" name="title" placeholder="Title" value="{{Scope.CommentTitle}}">
<textarea class="form-control" name="comment" rows="7" placeholder="Enter a new comment here">{{Scope.CommentContent}}</textarea>
<button type="submit" class="btn btn-primary btn-sm">Add Comment</a>
</div>
</form>
{{/main.session.AuthorRights.Comment}}
</p>
{{/WithComments}}
{{^WithComments}}
<p><a href="ArticleView?id={{Article.ID}}&withComments=true#comments" class="btn btn-primary btn-sm">Show Comments</a></p>
{{/WithComments}}
{{>footer}}

View File

@@ -1,15 +1,25 @@
{{>header}}
{{>masthead}}
<div class="blog-header">
<h1 class="blog-title">User {{Author.LogonName}}</h1>
<div class="lead blog-description">{{Author.FirstName}} {{Author.FamilyName}}
</div>
</div>
<div class="row">
<div class="col-sm-8 panel">
<div class="panel-heading">Information about <strong>{{Author.LogonName}}</strong></div>
<div class="panel-body">
{{{TSQLAuthor.HtmlTable Author}}}
</div>
<div class="blog-header">
<h1 class="blog-title">User {{Author.LogonName}}</h1>
<div class="lead blog-description">{{Author.FirstName}} {{Author.FamilyName}}
</div>
</div>
<div class="row">
<div class="col-sm-8 panel">
<div class="panel-heading">Information about <strong>{{Author.LogonName}}</strong></div>
<div class="panel-body">
{{{TSQLAuthor.HtmlTable Author}}}
{{#main.session}}
<form class="navbar-form" action="login" method="post">
<span class="navbar-text">Change <strong>{{AuthorName}}</strong> Password:</span>
<input type="text" class="form-control" name="LogonName" placeholder="Username">
<input type="password" class="form-control" name="PlainPassword" placeholder="Old Password">
<input type="password" class="form-control" name="NewPlainPassword1" placeholder="New Password">
<input type="password" class="form-control" name="NewPlainPassword2" placeholder="Repeat New Password">
<button class="btn" type="submit">Change</button>
</form>
{{/main.session}}
</div>
{{>articlerow}}
{{>footer}}

View File

@@ -22,8 +22,23 @@
</div><!-- container -->
<div class="blog-footer">
<p>Proudly using the Open Source <a href="http://mormot.net">mORMot ORM/SOA/MVC Framework</a>.<br>
{{{main.blog.engine}}}<br>
<small>{{{main.blog.copyright}}} - page generated in [[GENERATION_TIME_TAG]]</small></p>
<p><a href="#">Back to top</a></p>
<ul class="pager">
<li><a href="#">Back to top</a></li>
<li><a href=rss>RSS feed</a></li>
<li><a href=https://synopse.info/forum/viewtopic.php?id=25>Synopse</a></li>
<li><a href=https://synopse.info/forum>Support Forum</a></li>
<li><a href=https://github.com/synopse>On Github</a></li>
</ul>
{{#main.session}}
<p>
<form class="navbar-form" action="logout">
<span class="navbar-text">Signed in as <a href=AuthorView?id={{AuthorID}}>{{AuthorName}}</a></span>
<button class="btn" type="submit">Logout</button>
</form>
</p>
{{/main.session}}
</div>
</body>
</html>

View File

@@ -3,44 +3,23 @@
<div class="blog-masthead">
<div class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button class="navbar-toggle" type="button" data-toggle="collapse" data-target="#navbar-main">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<div class="navbar-left">
<a class="navbar-brand" href="default">{{main.blog.title}}</a>
</div>
<div class="navbar-form navbar-right">
<form action="articleMatch" method="post">
<div class="form-group">
<input type="text" class="form-control" name="match" placeholder="Search Expression">
</div>
</form>
</div>
</div>
<center>
<div class="navbar-collapse collapse" id="navbar-main">
{{#main.session}}
<div class="navbar-right">
<form class="navbar-form" action="logout">
<span class="navbar-text">Signed in as <a href=AuthorView?id={{AuthorID}}>{{AuthorName}}</a></span>
<button class="btn" type="submit">Logout</button>
</form>
</div>
{{/main.session}}
{{^main.session}}
<form class="navbar-form navbar-right" action="login" method="post">
<div class="form-group">
<input type="text" class="form-control" name="LogonName" placeholder="Username">
<input type="password" class="form-control" name="PlainPassword" placeholder="Password">
</div>
<button type="submit" class="btn btn-default">Sign In</button>
</form>
{{/main.session}}
</div>
</center>
<div class="navbar-header">
<button class="navbar-toggle" type="button" data-toggle="collapse" data-target="#navbar-main">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<div class="navbar-left">
<a class="navbar-brand" href="default">{{main.blog.title}}</a>
</div>
<div class="navbar-form navbar-right">
<form action="articleMatch" method="post">
<div class="form-group">
<input type="text" class="form-control" name="match" placeholder="Search Expression">
</div>
</form>
</div>
</div>
</div>
</div>
</div>