#51 mORMot 1 » Invalid Timestamp » 2017-08-30 08:47:54

esmondb
Replies: 2

I'm having a problem with invalid timestamps when making two requests in quick succession after logging in. The first request succeeds but the second one fails.

In the logs I've got: Invalid TimeStamp: expected >=-8, got 142

The problem seems to be in line 51786 of mormot.pas when result.fLastTimeStamp is less than fTimeStampCoherencyTicks, I assume causing an overflow:

if HexDisplayToCardinal(PTimeStamp,aTimeStamp) and
     (fNoTimeStampCoherencyCheck or (result.fLastTimeStamp=0) or
      (aTimeStamp>=result.fLastTimeStamp-fTimeStampCoherencyTicks)) then begin

could using abs() could be a solution?:

  (aTimeStamp>=abs(result.fLastTimeStamp-fTimeStampCoherencyTicks))) then begin

Also in line 51803 it's failing to log the timestamp value and looks like it should be:

Ctxt.Log.Log(sllUserAuth,'Invalid TimeStamp: expected >=%, got %',
      [result.fLastTimeStamp-fTimeStampCoherencyTicks,Int64(aTimeStamp)],self);  //<<< added Int64(aTimeStamp)

#52 Re: mORMot 1 » Javascript authentication » 2017-08-23 06:56:09

I think there was a bug in the original code. in the crc32 function try changing this line:

 crc = crc ^ (-1);

to this:

 crc = crc^0xFFFFFFFF; 

#54 Re: mORMot 1 » 2nd Edition of mORMot book » 2017-02-04 21:14:46

The photo looks like a Giacometti view of our mORMot!

#55 Re: mORMot 1 » TSMTPServer and ISMTPServerConnection usage » 2017-01-06 09:27:52

I've found that synapse works well:
http://synapse.ararat.cz/doku.php/
It supports attachments and SSL connections

#56 Re: mORMot 1 » compiling sqlite » 2016-10-04 10:04:51

I've just seen this post on the embarcadero web site: https://www.embarcadero.com/br/products … tarter-faq
Can this free edition be used to compile SQLite?

#58 Re: mORMot 1 » compiling sqlite » 2016-09-08 12:29:11

I'm only licensed up to XE2 so will probably use the pre-complied version here . But I see I can remove the 'rem's in the bat file to go back to the old Borland version.
Thanks,
smile Esmond

#59 mORMot 1 » compiling sqlite » 2016-09-08 09:17:04

esmondb
Replies: 11

I've noticed that c.bat in the sqlite folder has been edited and seems to point to a different compiler. Does it not use the free borland C++ builder 5.5 anymore?

#60 mORMot 1 » MVC sample 30 - computeminimalData » 2016-08-29 09:53:54

esmondb
Replies: 0

The dates for the sample data in the MVC example seem to be awry (using delphi 2007 on windows 7). The version of ComputeMinimalData below should be better.

procedure TBlogApplication.ComputeMinimalData;
var info: TSQLBlogInfo;
    article: TSQLArticle;
    comment: TSQLComment;
    tag: TSQLTag;
    batch: TSQLRestBatch;
    n,t: integer;
    articles,tags,comments: TIDDynArray;
    tmp: RawUTF8;
    auto: IAutoFree; // mandatory only for FPC
    tmpTime: TDateTime;
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
    info.Title := 'mORMot BLOG';
    info.Language := 'en';
    info.Description := 'Sample Blog Web Application using Synopse mORMot MVC';
    info.Copyright := '&copy;2016 <a href=http://synopse.info>Synopse Informatique</a>';
    info.About := TSynTestCase.RandomTextParagraph(30,'!');
    RestModel.Add(info,true);
  end;
  if RestModel.TableHasRows(TSQLArticle) then
    exit;
  tmp := StringFromFile('d:\download\2014-12-27-a8003957c2ae6bde5be6ea279c9c9ce4-backup.txt');
  if tmp<>'' then begin
    DotClearFlatImport(RestModel,tmp,fTagsLookup,'http://blog.synopse.info',
      (TMVCRunOnRestServer(fMainRunner).Views as TMVCViewsMustache).ViewStaticFolder);
    exit;
  end;  
  SetLength(tags,32);
  for n := 1 to length(tags) do begin
    tag.Ident := 'Tag'+UInt32ToUtf8(n);
    tag.IDValue := n*2; // force test TSQLTags layout
    tags[n-1] := RestModel.Add(tag,true,true);
  end;
  fTagsLookup.Init(RestModel); // reload after initial fill
  tmpTime := now - FAKEDATA_ARTICLESCOUNT;
  batch := TSQLRestBatch.Create(RestModel,TSQLArticle,20000);
  try
    article.Author := TSQLAuthor(1);
    article.AuthorName := 'synopse';
    for n := 1 to FAKEDATA_ARTICLESCOUNT do begin
      article.CreatedAt := TimeLogFromDateTime(tmpTime);
      article.ModifiedAt := article.CreatedAt;
      tmpTime := tmpTime +1;
      article.SetPublishedMonth(article.CreatedAt);
      //article.PublishedMonth := 2014*12+(n div 10);
      article.Title := TSynTestCase.RandomTextParagraph(5,' ');
      article.Abstract := TSynTestCase.RandomTextParagraph(30,'!');
      article.Content := TSynTestCase.RandomTextParagraph(200,'.','http://megascroll.net');
      article.Tags := nil;
      for t := 1 to Random(6) do
        article.TagsAddOrdered(tags[random(length(tags))],fTagsLookup);
      batch.Add(article,true,false,[],true);
    end;
    if RestModel.BatchSend(batch,articles)=HTTP_SUCCESS then begin
      fTagsLookup.SaveOccurence(RestModel);
      comment.Author := article.Author;
      comment.AuthorName := article.AuthorName;
      batch.Reset(TSQLComment,20000);
      for n := 1 to FAKEDATA_ARTICLESCOUNT*2 do begin
        comment.Article := Pointer(articles[random(length(articles))]);
        comment.Title := TSynTestCase.RandomTextParagraph(5,' ');
        comment.Content := TSynTestCase.RandomTextParagraph(30,'.','http://megascroll.net');
        batch.Add(Comment,true);
      end;
      RestModel.BatchSend(batch,comments)
    end;
  finally
    batch.Free;
  end;
end;

Another very small thing - The base timestamped record doesn't need to be declared. It could use TSQLRecordTimed in mormot.pas instead of TSQLRecordTimeStamped

#61 Re: mORMot 1 » [SOLVED] Help with connection/setup » 2016-08-22 14:11:20

Thanks,

Just realised that I need to add the jwoAsJsonNotAsString option to get ID instead of rowID.

#62 Re: mORMot 1 » [SOLVED] Help with connection/setup » 2016-08-22 11:37:14

I'm also puzzled by the difference between 'ID' and 'RowID'.

If I use TSQLTableDB.getJSONValues(true) it returns ID

but with TSQLRecord.getJSONValues(true,true,soSelect) it returns RowID for the record number.

Can anyone explain the difference between ID and RowID?

Thanks,
Esmond

#63 mORMot 1 » sanitize HTML » 2016-07-30 10:55:53

esmondb
Replies: 1

I've been playing with sample 30 (MVC Server) and noticed it's lacking much of a UI for editing blogs. www.salesforce.com have released some open source projects which look very professional and could help provide a javascript/html/bootstrap interface:

http://getfuelux.com/ has a pillbox component which could provide editing for tags. It also has a good looking datepicker.

http://beta.quilljs.com/ is a javascript editor which could provide an HTML editing mechanism - I'm sure someone will ask for this.

I guess the difficulty with quill and allowing HTML posts is malicious javascript injection etc. - especially if unknown users make comments.

Would these feature requests for the mustache unit make sense?:

1/ as well as escaped and non-escaped HTML variables could there be a third class of variable which is HTML white-listed-escaped? i.e. allow a few html tags to get through with limited attributes, a bit like quill. Something similar in javascript is http://htmlpurifier.org/. This seems better than BBcode as long as the white list is documented.

2/ hive off the section of code which escapes html in mustache.pas so that SOA functions could do this on-write rather than on-read (or is this premature optimization?).

#65 Re: mORMot 1 » Why HTTP_RESP_STATICFILE (Ctxt.ReturnFile) not gzipped? » 2016-04-18 06:35:02

Probably the 'static content compression' feature needs to be turned on in IIS.

#67 mORMot 1 » RecordLoadJSON and TTimeLog » 2016-04-14 15:46:14

esmondb
Replies: 2

I can't get RecordLoadJSON to work with records containing TTimeLog.
But it seems to work if TTimeLog is defined as int64 (using delphi 2007).
I've put an example program below.

program test;

{$APPTYPE CONSOLE}

uses
  SysUtils, SynCommons;

type
  myJSON = packed record
    myDate: TTimeLog;
    s: string; //added to create some TypeInfo
  end;

const
  __myJSON = 'myDate:TTimeLog; s:string';
  testString = '{"myDate":135305815903}';

var
  myRecord: myJSON;

begin
  TTextWriter.RegisterCustomJSONSerializerFromText(TypeInfo(myJSON),__myJSON);
  RecordLoadJSON(myRecord,testString,TypeInfo(myJSON));

  writeln(DateToStr(TimeLogToDateTime(myRecord.myDate))); //outputs 12/30/1899
         //if myDate is defined as an Int64 then output is correct - 4/14/2016

  writeln(DateToStr(TimeLogToDateTime(135305815903)));     //outputs 4/14/2016

  readln;

end.

Is this a bug?

#68 Re: mORMot 1 » MVC/MVVM Web Applications with mORMot! » 2016-03-24 08:21:02

Thanks, I need to study the source code more.

#69 Re: mORMot 1 » MVC/MVVM Web Applications with mORMot! » 2016-03-22 16:36:47

Sorry, my question wasn't very clear. I've got a server which is basically defined like this:

type
  TMyServer = class(TSQLRestServerDB)
  published
    procedure service_1(cTxt: TSQLRestServerURIContext);
    procedure service_2(cTxt: TSQLRestServerURIContext);
    ...
  end;

This is implemented with method-based services. Is there a way to use mORMotMVC.pas without rewriting the services to use interfaces?

#70 Re: mORMot 1 » MVC/MVVM Web Applications with mORMot! » 2016-03-22 12:26:27

Is it possible to use TMVCApplication with method based services instead of interface based services?

#71 Re: mORMot 1 » Javascript authentication » 2016-02-05 14:23:30

Thanks for adding this option.

I slightly dread trying to handle 64bit IDs in javascript. My plan at the moment is to ignore IDs bigger than javascript's 53-bit limit for javascript clients - it's unlikely I'll need anything larger than 32bit.

#72 Re: mORMot 1 » THttpApiServer Error "HTTP Error 400. The request URL is invalid." » 2016-02-03 11:25:46

Normally about 2000 chars in a URL is fine. Not sure why you have the 260 char limit.

Only solution I can think of is to find a way to shorten the URL

#73 Re: mORMot 1 » Javascript authentication » 2016-01-25 12:52:11

I've put an improved version of my typescript client on GitHub:

https://github.com/esmondb/mORMot-Typescript-client

No guarantees for 'fitness for purpose' but any feedback welcome!

#74 Re: mORMot 1 » Smart Mobile Studio » 2016-01-22 10:24:18

I'm not completely sold on twitter's bootstrap. OpenUI5 and vcljs look really interesting. I tried bootstrap version 2, but needed to make quite a few tweaks for my use case. Then version 3 broke my tweaks and changed default css box-sizing causing more problems. Version 4 seems delayed.

#75 Re: mORMot 1 » what is sqlite4 » 2016-01-17 13:47:45

It doesn't seem that active looking at the timeline:
http://sqlite.org/src4/timeline
But the Decimal Math feature looks interesting

#76 Re: mORMot 1 » FTS problem » 2016-01-11 14:41:11

Great smile Thanks, it's all working now.

#77 Re: mORMot 1 » FTS problem » 2016-01-11 12:12:27

Many thanks smile. It's working fine now.

This probably isn't related but I can't get the main sample 30 (MVC Blog) to run when compiled on Delphi 2007 and win7 64bit. It's giving an AV in the ComputeMinimalData procedure in MVCViewModel.pas. Line 207:

if RestModel.BatchSend(batch,articles)=HTML_SUCCESS then begin

calls TSQLRestServerDB.MainEngineAdd in mORMotSQLite3.pas which causes an access violation at line 828:

AddInt64(TInt64DynArray(fBatchID),fBatchIDCount,result);

Having trouble finding the solution sad. Any help welcome!

#78 Re: mORMot 1 » FTS problem » 2016-01-10 08:13:27

Thanks but sorry I made a mistake in TSQLRecord.GetSQLCreate. Could the new fix below be applied.

This will probably break the search feature in sample 30 - MVC Server, for non-sqlite dbs. I guess this would need contentless FTS tables as before but with only an insert trigger and some sort of utility function to periodically update the FTS index by rebuilding it.

I wasn't quite sure why the fields in the main record were being concatenated into a single field in the fts record so I made a small change to allow multiple fts fields. If there is only one field in the FTS record it behaves as before and concatenates the main record fields, otherwise the fields in the FTS record definition must be the same or a subset of the main record's fields and they will be mapped across.


28695,28696c28695
<             aModel.Tables[Props.fFTSWithoutContentTableIndex].SQLTableName+'",'+
<             Props.fFTSWithoutContentExpression+',';
---
>             aModel.Tables[Props.fFTSWithoutContentTableIndex].SQLTableName+'",';
28705,28706c28704
<           if Props.fFTSWithoutContentExpression='' then
<             result := result+Name+',';
---
>           result := result+Name+',';
30083c30081,30083
<         exp := exp+'||'' ''||new.'+ContentTableFieldNames[i];
---
>         if high(ContentTableFieldNames) > 0 then
>           exp := exp+',new.'+ ContentTableFieldNames[i] else
>           exp := exp+'||'' ''||new.'+ContentTableFieldNames[i];
44200c44200
<     fts,main,ftsmainfield: RawUTF8;
---
>     fts,main,ftsfields: RawUTF8;
44211c44211
<   ftsmainfield := Props.Props.MainFieldName(true);
---
>   ftsfields := Props.Props.SQLTableSimpleFieldsNoRowID;
44221c44221
<     [main,main,fts,ftsmainfield,Props.fFTSWithoutContentExpression]);
---
>     [main,main,fts,ftsfields,Props.fFTSWithoutContentExpression]);
44224c44224
<     [main,main,fts,ftsmainfield,Props.fFTSWithoutContentExpression]);
---
>     [main,main,fts,ftsfields,Props.fFTSWithoutContentExpression]);

#79 Re: mORMot 1 » FTS problem » 2016-01-08 05:42:02

Changing the following two lines in TSQLRecord.GetSQLCreate seems to fix the problem

28690 result := result+'content="'+aModel.Tables[Props.fFTSWithoutContentTableIndex].SQLTableName+'",'+Props.fFTSWithoutContentExpression+',';
28698 else if Props.fFTSWithoutContentExpression='' then

Could this patch be applied to mormot.pas?

28690c28690
<         result := result+'content="",';
---
>         result := result+'content="'+aModel.Tables[Props.fFTSWithoutContentTableIndex].SQLTableName+'",'+Props.fFTSWithoutContentExpression+',';
28698c28698
<             [self,Name]) else
---
>             [self,Name]) else if Props.fFTSWithoutContentExpression='' then

#80 Re: mORMot 1 » FTS problem » 2016-01-07 06:00:21

Looking at the sqlite docs http://www.sqlite.org/fts3.html#*fts4content I noticed

It is not possible to UPDATE or DELETE a row stored in a contentless FTS4 table. Attempting to do so is an error.

However, TSQLRecordFTS4.InitializeTable seems to set up DELETE and INSERT triggers on contentless FTS4 tables.

Should the table be created as an External Content FTS4 Table instead of contentless? (see https://www.sqlite.org/fts3.html#section_6_2_2)

#81 mORMot 1 » FTS problem » 2016-01-06 11:03:35

esmondb
Replies: 8

I'm having a problem getting FTS4WithoutContent to work when updating records. It's giving this error: 'SQL logic error or missing database'.

I've put a test program below. Is the error from mormot or have I done something wrong?

program Project1;

{$APPTYPE CONSOLE}

uses
  SysUtils, mORMot, SynCommons, mORMotSQLite3, SynSQLite3Static;

type
  TSQLMy_Record = class(TSQLRecord)
  private
    fName: RawUTF8;
  published
    property Name: RawUTF8 read fName write fName;
  end;

  TSQLMy_FTSRecord = class(TSQLRecordFTS4)
  private
    fName: RawUTF8;
  published
    property Name: RawUTF8 read fName write fName;
  end;

function CreateModel: TSQLModel;
begin
  result := TSQLModel.Create([TSQLMy_Record, TSQLMy_FTSRecord]);
  result.Props[TSQLMy_FTSRecord].FTS4WithoutContent(TSQLMy_Record, [
    'Name']);
end;

var
  Model: TSQLModel;
  Rest: TSQLRestServerDB;
  rec: TSQLMy_Record;

begin
    Model := CreateModel;
    Rest := TSQLRestServerDB.Create(Model,'test.db3');
    Rest.CreateMissingTables();
    rec := TSQLMy_Record.Create;
    rec.Name := 'Barack Obama';
    Rest.Add(rec,True);
    rec.Name := 'President Barack Obama';
    Rest.Update(rec);
    rec.Free;
    Rest.Free;
    Model.Free;
    readln;
end.

#82 Delphi » Delphi Build » 2015-12-23 09:33:54

esmondb
Replies: 1

Hi AB,

Could I make a request for a blog post?

I've noticed your post in this forum: http://delphicodemonkey.blogspot.co.uk/ … -deux.html

What I would like is a KISS example of how to build delphi projects with a ms batch file. I've looked at MSBuild and got confused.

#83 Re: mORMot 1 » FixInsight » 2015-12-07 14:11:54

Thanks for the extra insight! I haven't had time to test any other units.

#84 mORMot 1 » FixInsight » 2015-12-07 11:36:57

esmondb
Replies: 2

I don't normally use IDE plug-ins but has anyone found FixInsight useful?

Trying it out on mormot.pas doesn't seem to bring up any serious issues.

There are a few empty else blocks and a couple of missing 'inherited;' statements in overriden constructors which I assume are harmless.

And these warnings which I also assume are harmless:

[FixInsight Warning] mORMot.pas(34128): W509 Unreachable code
[FixInsight Warning] mORMot.pas(48157): W509 Unreachable code
[FixInsight Warning] mORMot.pas(49273): W515 Suspect FREE call

#85 Re: mORMot 1 » Unique validation on ajax request » 2015-11-30 12:58:19

I had a similar problem with a service and chose 409 as the http error code. I'm a bit confused with how to handle mORMot errors with ajax. Are errors within mORMot only returned with an ok 200 response with the error encoded as JSON within the response or does it use http error codes?

#86 Re: mORMot 1 » fts4aux » 2015-10-21 05:58:59

Thanks, yes it does work. Doing wildcard style searches isn't easy but I've found this works -

SELECT term FROM ft_terms WHERE term BETWEEN 'appl' AND 'appl' ||
CAST(x'FF' AS CHAR)

#87 mORMot 1 » fts4aux » 2015-10-20 10:55:54

esmondb
Replies: 3

Does mORMot have any features to make use of fts4aux: https://www.sqlite.org/fts3.html#fts4aux ?

I've noticed it metioned in the update below but can't see how to use it with mORMot.
http://synopse.info/forum/viewtopic.php?id=299

Thanks

#88 mORMot 1 » TSQLRest.UpdateField with TIntegerArray » 2015-08-27 20:41:42

esmondb
Replies: 1

I'm trying to update a TintegerArray field with TSQLRest.UpdateField but get this error: 'ESynException with message "TJSONSerializer.AddVariant(VType=8195)".'

TSQLRest.Update with the whole record works fine. (using SQLite)

What's wrong?

#89 mORMot 1 » IntegerDynArrayToCSV » 2015-08-27 20:40:01

esmondb
Replies: 1

Could IntegerDynArrayToCSV change it's behaviour so that the Prefix and Suffix are returned with an empty array?

It would need the following changes at line 22603 of SynCommons.pas:

function IntegerDynArrayToCSV(const Values: array of integer; ValuesCount: integer;
  const Prefix: RawUTF8=''; const Suffix: RawUTF8=''): RawUTF8;
type
  TInts16 = packed array[word] of string[15]; // shortstring are faster (no heap allocation)
var i, L, Len: PtrInt;
    tmp: array[0..15] of AnsiChar;
    ints: ^TInts16;
    P: PAnsiChar;
begin
  result := '';
  if ValuesCount=0 then
    exit;
...

to

function IntegerDynArrayToCSV(const Values: array of integer; ValuesCount: integer;
  const Prefix: RawUTF8=''; const Suffix: RawUTF8=''): RawUTF8;
type
  TInts16 = packed array[word] of string[15]; // shortstring are faster (no heap allocation)
var i, L, Len: PtrInt;
    tmp: array[0..15] of AnsiChar;
    ints: ^TInts16;
    P: PAnsiChar;
begin
  result := '';
  if ValuesCount=0 then begin
    result := Prefix + Suffix;
    exit;
  end;
...

The following Int64DynArrayToCSV function would need the same change for consistency.

#90 Re: mORMot 1 » Implementing your own password hash » 2015-07-27 12:34:08

Maybe there is something wrong with TSQLAuthGroup.SQLAccessRights. Perhaps try setting it to:

const
  SERVICE_ACCESS_RIGHTS: TSQLAccessRights =
    (AllowRemoteExecute: [reService];
    GET: []; POST: [];
    PUT: []; DELETE: []);

#91 Re: mORMot 1 » Implementing your own password hash » 2015-07-27 11:58:25

I got the 403 error in a similar situation the other day. I had forgotten to set TSQLAuthGroup.SessionTimeout which reverted to 0.

#92 Re: mORMot 1 » TSQLRecord.Validate » 2015-07-25 10:23:12

Thanks, understand it better now and all works. I was doing validation server side as I was using ajax and checking for a unique field.

#93 Re: mORMot 1 » TSQLRecord.Validate » 2015-07-21 16:39:46

thought of using StringToUTF8() but was thinking it would be more efficient to do the function on the client with UTF8ToString() instead of the server.

I'll have a look at LoadResStringTranslate().

Thanks

#94 mORMot 1 » TSQLRecord.Validate » 2015-07-21 15:25:55

esmondb
Replies: 4

I'm using TSQLRecord.Validate in a service. Would it be possible to change it to return rawUTF8 instead of string?

Also what's the best way to override the resourcestrings in SynCommons.pas?

Many thanks

#95 Re: mORMot 1 » User security » 2015-07-21 15:24:28

Many thanks. I think a need to create a service.

#96 mORMot 1 » JSON empty response » 2015-07-21 04:26:54

esmondb
Replies: 0

This probably doesn't matter but firebug gives a silent parsing error with TSQLRestServerURIContext.Success(). Should the JSON content-type header be omitted with Success() as an empty string isn't valid JSON?

#97 mORMot 1 » User security » 2015-07-10 20:38:46

esmondb
Replies: 2

I'm using mORMot with sqlite and the default authentication using TSQLAuthUser.

I've created a parallel table to store extended user details like contact details etc. What's the best way to protect this table so that a user can only view and update their own record? Or would it be easier to extend TSQLAuthUser and use a service to access this user record?

#99 mORMot 1 » Session Create returning Group Rights ID » 2015-07-06 16:08:15

esmondb
Replies: 2

Could SessionCreate in mORMot.pas be changed to return also the user's group id? I'm using an ajax client and want to adjust the UI depending on group.

Changing line 43821 seems the simplest way:

procedure TSQLRestServerAuthentication.SessionCreate(Ctxt: TSQLRestServerURIContext;
  var User: TSQLAuthUser);
var Session: TAuthSession;
begin
  if User<>nil then
  try // now client is authenticated -> create a session
    fServer.SessionCreate(User,Ctxt,Session); // call Ctxt.AuthenticationFailed on error
    if Session<>nil then
      // CHANGED LINE 43821:
      Ctxt.Returns(['result',Session.fPrivateSalt,'logonname',Session.User.LogonName,'groupid',Session.User.GroupRights.ID]);
  finally
    User.Free;
  end;
end;

#100 Re: Delphi » GetIt.RealName := 'GetItIfItWontHurtOurSells' » 2015-06-07 14:01:54

I think they should give away the professional version of Delphi to students and hobbyists but on the understanding it's only used for non-commercial uses. I'm sure 99% of people would upgrade if they started making money from it. PKzip, for example, was shareware but was apparently profitable to it's company.

My first version of Delphi was version 3 which I got on the cover of a UK computer mag. I just paid £20 for the printed docs. Then bought Delphi 5 as a shrink wrapped box off eBay and upgraded it a couple of times. If I'd had to pay the full price I wouldn't have used it.

Board footer

Powered by FluxBB