![]() |
NetUserEnum die 2.
Hi,
entweder ich bin blöd, oder der Kompiler ist kaputt. Also, ersteinmal meine Definition der NetUserEnum aus JwaLM:
Delphi-Quellcode:
Ich habe mir bereits Christian's Definition aus dem Ursprungsthread angesehen. Allerdings kann ich damit nix anfangen. Darum bitte ich euch mir hier nochmal zu helfen, denn ich steige nicht mehr durch.
function NetUserEnum(
const servername: LPCWSTR; const level: DWORD; const filter: DWORD; const bufptr: Pointer; const prefmaxlen: DWORD; var entriesread: DWORD; var totalentries: DWORD; const resume_handle: PDWORD ): NET_API_STATUS; stdcall; Nun aber mein Source (nachdem ich alles in eine Funktion schreiben wollte, ging nix mehr):
Delphi-Quellcode:
So, wenn ich nun ausführe: Es gibt ne saftige AccessViolation mit folgendem Inhalt:
procedure ListAllUsers(hTreeView: DWORD; hMasterParent: Cardinal; var iEntriesRead: Cardinal);
type PUSER_INFO_0 = ^_USER_INFO_0; // PUSER_INFO_11 = ^_USER_INFO_11; <= für später... var aTempIStruct: TTVInsertStruct; aUserBuffer, aWorkBuffer: PUSER_INFO_0; dwEntriesRead: Cardinal; i: integer; sTemp: string; begin if NetUserEnum(nil, 0, FILTER_NORMAL_ACCOUNT, @aUserBuffer, MAX_PREFERRED_LENGTH, dwEntriesRead, dwEntriesRead, nil) = NERR_SUCCESS then begin aWorkBuffer := aUserBuffer; for i:=1 to dwEntriesRead do begin aTempIStruct.hParent := Pointer(hMasterParent); sTemp := aWorkBuffer.usri0_name; aTempIStruct.item.pszText := @sTemp[1]; SendMessage(hTreeView, TVM_INSERTITEM, 0, integer(@aTempIStruct)); inc(aWorkBuffer, sizeOf(PUSER_INFO_0)); end; end; NetAPIBufferFree(aUserBuffer); end;
Code:
Dann wird das Programm zurückgesetzt und Ende.
---------------------------
Application Error --------------------------- Exception EAccessViolation in module OSInfo.exe at 000037C3. Access violation at address 004037C3 in module 'OSInfo.exe'. Read of address BAADF00D. --------------------------- OK --------------------------- Was mache ich nur falsch. :cry::cry::cry::cry::cry: Chris |
Code:
Du läufst 2 Stellen zu weit. Du mußt die Schleife von 0 bis dwEntriesRead-1 laufen lassen.
for i:=1 to dwEntriesRead do begin
|
Wie bereits per ICQ mitgeteilt: hat nix gebracht...
Chris |
Nach ein paar Unterredungen in ICQ ist folgendes herausgekommen:
Delphi-Quellcode:
Allerdings ist hier die ursprüngliche Deklaration von NetUserEnum bei zubehalten. Ich hatte sie geändert, da Christian das so gepostet hat (oder ich habe ihn missverstanden).
procedure ListAllUsers(hTreeView: DWORD; hMasterParent: Cardinal; var iEntriesRead: Cardinal);
{type PUSER_INFO_0 = ^_USER_INFO_0; PUSER_INFO_11 = ^_USER_INFO_11;} var aTempIStruct: TTVInsertStruct; aUserBuffer, aWorkBuffer: Pointer; //PUSER_INFO_0; dwEntriesRead: Cardinal; i: integer; sTemp: string; begin aTempIStruct.item.mask := TVIF_TEXT; if NetUserEnum(nil, 0, FILTER_NORMAL_ACCOUNT, aUserBuffer, MAX_PREFERRED_LENGTH, dwEntriesRead, dwEntriesRead, nil) = NERR_SUCCESS then begin aWorkBuffer := aUserBuffer; for i:=0 to dwEntriesRead-1 do begin aTempIStruct.hParent := Pointer(hMasterParent); sTemp := PUserInfo0(aWorkBuffer)^.usri0_name; aTempIStruct.item.pszText := @sTemp[1]; SendMessage(hTreeView, TVM_INSERTITEM, 0, integer(@aTempIStruct)); inc(Integer(aWorkBuffer), sizeOf(aWorkBuffer)); end; end; NetAPIBufferFree(aUserBuffer); end; Nochmal danke @ Luckie.. Chris |
Moin Chris,
das hier ist schlicht falsch:
Delphi-Quellcode:
Es muss schlicht
inc(aWorkBuffer, sizeOf(PUSER_INFO_0));
Delphi-Quellcode:
heissen.
inc(aWorkBuffer);
Da aWorkBuffer typisiert ist (eben PUSER_INFO_O), zählt der Compiler bei einem inc(aWorkBuffer) automatisch um SizeOf(PUSER_INFO_0) hoch. Nur wenn der Typ Pointer ist, muss man sich um die Grösse der Struktur selber Gedanken machen. Das kannst Du auch in meinen Beiträgen zu dem Ursprungsthread auch schon sehen (auch wenn ich es dort wohl nicht explizit dazugeschrieben hatte. ;-) @Luckie: Da man mit i hier nicht direkt einen Tabelleneintrag indiziert spielt es keine Rolle, ob man i nun von 0 bis dwEntriesRead-1, von 1 bis dwEntriesRead oder von 278 bis dwEntriesRead+277 laufen lässt. Hauptsache die Gesamtzahl der Schleifendurchläuft ist korrekt. |
Hi Christian,
habe das gerade noch geändert... Lese mir aber gleich nochmal alle Threads zu diesem Thema durch... Chris |
Stimmt hast recht, Aber bei solchen for-Schleifen hab eich mir das imme rangewöhnt, dann vergist man es an anderer Stelle nicht. :wink:
|
Moin Chris,
die Lösung, die Du gepostet hast, während ich meinen Beitrag geschrieben habe funktioniert natürlich auch. Ich würde diese allerdings in die Rubrik: "Warum einfach, wenn's umständlich geht" einsortieren ;-) |
Oops: Es muss wohl eher
Delphi-Quellcode:
heißen, da sonst:
inc(integer(aWorkBuffer));
Zitat:
|
Dafür kann man aber bei meiner Lösung die Jedi-Header-Übersetzungen in Ruhe lassen. :wink:
|
Ätsch, Bätsch ausgetrickst. Wenn ich
Delphi-Quellcode:
mache gibts kein Problem, aber nachdem ich nun einfach
inc(integer(aWorkBuffer), sizeOf(PUSERINFO0));
Delphi-Quellcode:
mache, dann gibts ne' AccessViolation...
inc(aWorkBuffer); {oder} inc(integer(aWorkBuffer));
Chris |
Moin Chris,
nein, muss es nicht. So hab' ich das gemacht
Delphi-Quellcode:
und es funktioniert einwandfrei.
function NetUserEnum(
const servername: LPCWSTR; const level: DWORD; const filter: DWORD; const bufptr: Pointer; const prefmaxlen: DWORD; var entriesread: DWORD; var totalentries: DWORD; const resume_handle: PDWORD ): NET_API_STATUS; stdcall; external 'netapi32.dll'; function NetApiBufferFree(const pBuffer : Pointer): NET_API_STATUS; stdcall; external 'netapi32.dll'; procedure TfrmMAIN.miFileOpenClick(Sender: TObject); var rui0Work : PUSER_INFO_0; rui0Save : PUSER_INFO_0; dwEntriesRead : DWORD; i : integer; begin if NetUserEnum(nil, 0, FILTER_NORMAL_ACCOUNT, @rui0Work, MAX_PREFERRED_LENGTH, dwEntriesRead, dwEntriesRead, nil) = NERR_SUCCESS then begin rui0Save := rui0Work; for i:=1 to dwEntriesRead do begin ListBox1.Items.Add(rui0Work.usri0_name); [color=red]inc(rui0Work);[/color] end; end; NetAPIBufferFree(rui0Save); end; (Ich hab' sogar mal die Funktionsdeklarationen benutzt, wie es die Jedis machen) |
HILFE!!!! Ich drehe hier noch durch. Also, wenn ich das mache, was du gemacht hast, dann kommt:
Zitat:
Chris |
Moin Luckie,
dann nimm meinetwegen die Jedi Units:
Delphi-Quellcode:
Mein Methode funktioniert trotzdem ;-)
procedure TfrmMAIN.miFileOpenClick(Sender: TObject);
type PUserInfo0 = ^TUserInfo0; _USER_INFO_0 = record usri0_name: LPWSTR; end; TUserInfo0 = _USER_INFO_0; USER_INFO_0 = _USER_INFO_0; var rui0Work : PUserInfo0; rui0Save : PUserInfo0; dwEntriesRead : DWORD; i : integer; begin if NetUserEnum(nil, 0, FILTER_NORMAL_ACCOUNT, @rui0Work, MAX_PREFERRED_LENGTH, dwEntriesRead, dwEntriesRead, nil) = NERR_SUCCESS then begin rui0Save := rui0Work; for i:=1 to dwEntriesRead do begin ListBox1.Items.Add(rui0Work.usri0_name); inc(rui0Work); end; end; NetAPIBufferFree(rui0Save); end; (Auch ohne die Jedi Deklarationen anzufassen) |
Also, nochmal *wimmer*:
Delphi-Quellcode:
Kommentare deswegen, dass ich alles schnell wiederrückgängig machen kann. ;)
procedure ListAllUsers(hTreeView: DWORD; hMasterParent: Cardinal; var iEntriesRead: Cardinal);
var aTempIStruct: TTVInsertStruct; aUserBuffer, aWorkBuffer: PUSERINFO0; dwEntriesRead: Cardinal; i: integer; sTemp: string; begin aTempIStruct.item.mask := TVIF_TEXT; {27 =>} if NetUserEnum(nil, 0, FILTER_NORMAL_ACCOUNT, @aUserBuffer{<=}, MAX_PREFERRED_LENGTH, dwEntriesRead, dwEntriesRead, nil) = NERR_SUCCESS then begin aWorkBuffer := aUserBuffer; for i:=0 to dwEntriesRead-1 do begin aTempIStruct.hParent := Pointer(hMasterParent); sTemp := {PUserInfo0(}aWorkBuffer{)^}.usri0_name; aTempIStruct.item.pszText := @sTemp[1]; SendMessage(hTreeView, TVM_INSERTITEM, 0, integer(@aTempIStruct)); inc({integer(}aWorkBuffer{), sizeOf(PUSERINFO0)}); end; end; NetAPIBufferFree(aUserBuffer); end; Nun, wenn ich das so mache (die Typen-deklarationen sind bereits in den Units drin; das gleiche, wenn ich sie reinschreibe), dann gibts den Fehler: Zitat:
|
Moin Chris,
könntest Du bitte auch noch mal markieren, was denn Zeile 27 ist? |
Das ist die Zeile in der @aUserBuffer steht. Und an der Stelle steht auch der Cursor... Markieren kann ich auch nochmal machen
Chris |
Moin Chris,
ich dachte die weiter oben gepostete Funktionsdeklaration stamme aus den Jedi Headern, aber die sieht ja so aus:
Delphi-Quellcode:
Wenn hier das var weggelassen würde, würde es auch gehen.
function NetUserEnum(servername: LPCWSTR; level: DWORD; filter: DWORD;
var {<=== VAR statt CONST} bufptr: Pointer; prefmaxlen: DWORD; var entriesread: DWORD; var totalentries: DWORD; resume_handle: PDWORD): NET_API_STATUS; stdcall; Ersatzweise musst Du für @aUserBuffer Pointer(aUserBuffer) schreiben, dann geht's. |
OK. Aber es wäre theoretisch möglich, die Methode von Luckie (2. Source, der anfängt mit "Nach ein paar Unterredungen in ICQ ist folgendes herausgekommen:") zu nutzen, richtig? Denn dann würde ich doch diese Methode nehmen, da diese diejenige ist, dir mir derzeit am ehesten zusagt.
Chris |
Moin Chris,
den hab' ich jetzt zwar nicht ausprobiert, bin aber der Meinung, dass der natürlich auch gehen würde. Wann man den Typecast nun macht spielt ja keine Rolle. Ich find's halt nur einfacher und übersichtlicher den nur einmal zu machen, statt öfter. |
Re: NetUserEnum die 2.
Hab's jetzt hinbekommen!
Vielen Dank! :dp: //EDIT: Sorry...........falscher Thread! |
AW: NetUserEnum die 2.
ich finde bei mir den fehler leider nicht. es kommt eine unerklärliche AV beim lesen von irgendetwas. es wird der nutzername Administrator in meine ListBox geschrieben mehr aber nicht. das ist erst passiert als ich das record _USER_INFO_0 um password, password_age und home_dir erweitert habe:
Delphi-Quellcode:
unit Unit1;
interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ComCtrls; type TForm1 = class(TForm) ListView1: TListView; ListBox1: TListBox; procedure FormCreate(Sender: TObject); private { Private-Deklarationen } public { Public-Deklarationen } end; var Form1: TForm1; const NERR_Success = 0; function NetApiBufferAllocate(ByteCount: DWORD; var Buffer: Pointer): DWORD; stdcall; external 'netapi32.dll'; function NetGetDCName(servername: LPCWSTR; domainname: LPCWSTR; bufptr: Pointer): DWORD; stdcall; external 'netapi32.dll'; function NetApiBufferFree(Buffer: Pointer): DWORD; stdcall; external 'netapi32.dll'; function NetWkstaGetInfo(servername: LPWSTR; Level: DWORD; bufptr: Pointer): Longint; stdcall; external 'netapi32.dll' name 'NetWkstaGetInfo'; function NetUserEnum(servername: LPCWSTR; Level: DWORD; filter: DWORD; var bufptr: Pointer; prefmaxlen: DWORD; var entriesread: DWORD; var totalentries: DWORD; resume_handle: PDWORD): DWORD; stdcall; external 'netapi32.dll'; type WKSTA_INFO_100 = record wki100_platform_id: DWORD; wki100_computername: LPWSTR; wki100_langroup: LPWSTR; wki100_ver_major: DWORD; wki100_ver_minor: DWORD; end; LPWKSTA_INFO_100 = ^WKSTA_INFO_100; _USER_INFO_0 = record usri1_name: LPWSTR; usri1_password: LPWSTR; usri1_password_age: DWORD; usri1_home_dir: LPWSTR; end; TUserInfo0 = _USER_INFO_0; function NetUserChangePassword(Domain: PWideChar; UserName: PWideChar; OldPassword: PWideChar; NewPassword: PWideChar) : Longint; stdcall; external 'netapi32.dll' name 'NetUserChangePassword'; implementation {$R *.dfm} function GetNetParam(AParam: integer): string; var PBuf: LPWKSTA_INFO_100; Res: Longint; begin result := ''; Res := NetWkstaGetInfo(nil, 100, @PBuf); if Res = NERR_Success then begin case AParam of 0: result := string(PBuf^.wki100_computername); 1: result := string(PBuf^.wki100_langroup); end; end; end; function GetComputerName: string; begin result := GetNetParam(0); end; function GetDomainName: string; begin result := GetNetParam(1); end; function GetDomainControllerName(const ADomainName: string): string; var wDomainName: WideString; Controller: PWideChar; begin wDomainName := ADomainName; NetGetDCName(nil, PWideChar(wDomainName), @Controller); result := WideCharToString(Controller); NetApiBufferFree(Controller); end; procedure GetUsers(Users: TStrings; AServer: string); type TUserInfoArr = array [0 .. 3] of TUserInfo0; var UserInfo: Pointer; entriesread, totalentries, ResumeHandle: DWORD; Res: DWORD; i: integer; FServer: WideString; begin FServer := AServer; ResumeHandle := 0; repeat Res := NetUserEnum(PWideChar(FServer), 0, 0, UserInfo, 64 * 3, entriesread, totalentries, @ResumeHandle); if (Res = NERR_Success) or (Res = ERROR_MORE_DATA) then begin for i := 0 to entriesread - 1 do Users.Add(TUserInfoArr(UserInfo^)[i].usri1_name); NetApiBufferFree(UserInfo); end; until Res <> ERROR_MORE_DATA; end; function isPw(const User, Password: string): Boolean; var Err: LongWord; begin Err := NetUserChangePassword(nil, PWideChar(WideString(User)), PWideChar(WideString(Password)), PWideChar(WideString(Password))); result := Err = 0; end; procedure TForm1.FormCreate(Sender: TObject); begin GetUsers(ListBox1.Items, ''); end; end. |
AW: NetUserEnum die 2.
Und warum wird schonwieder alles Wichtige unterschlagen?
Wie genau lautet die Fehlermeldung? (der Vorschlag mit Strg+C ist immernoch sehr zu empfehlen) An welcher Stelle tritt sie auf? (auch wenn meine :glaskugel: schon fast eine Vermutung hat) Wie kommst du auf die saudämliche Idee diesen Record verändern zu wollen und warum hast du genau diese neuen Felder dort so reingemacht? Das Format dieser Puffer ist genau definiert und der Aufbau ist über ![]() Da du immernoch den Info-Level 0 abfragst, muß es zwangsweise schief gehn, denn dort ist ausschließlich das Feld "name" vorhanden. :roll: |
AW: NetUserEnum die 2.
Delphi-Quellcode:
Anwendungsbeispiel für meine Unit MpuNTUser:
unit Unit1;
interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) ListBox1: TListBox; Button1: TButton; procedure Button1Click(Sender: TObject); private { Private-Deklarationen } public { Public-Deklarationen } end; const NERR_Success = 0; MAX_PREFERRED_LENGTH = DWORD(-1); type NET_API_STATUS = DWORD; TEnumUers = function(Username: string; cntUsers: Integer; Data: Pointer): Boolean; TUserInfo1 = record usri1_name: LPWSTR; usri1_password: LPWSTR; usri1_password_age: DWORD; usri1_priv: DWORD; usri1_home_dir: LPWSTR; usri1_comment: LPWSTR; usri1_flags: DWORD; usri1_script_path: LPWSTR; end; PUserInfo1 = ^TUserInfo1; function NetUserEnum(servername: LPCWSTR; level: DWORD; filter: DWORD; var buf: Pointer; prefmaxlen: DWORD; var entriesred: DWORD; var totalentries: DWORD; resumehandle: PDWORD): NET_API_STATUS; stdcall; function NetApiBufferFree(Buffer: Pointer): NET_API_STATUS; stdcall; var Form1: TForm1; implementation const netapi32lib = 'netapi32.dll'; function NetUserEnum; external netapi32lib name 'NetUserEnum'; function NetApiBufferFree; external netapi32lib name 'NetApiBufferFree'; {$R *.dfm} function EnumUsersCallback(Username: string; cntUsers: Integer; Data: Pointer): Boolean; begin TListBox(Data).Items.Add(Username); Result := True; end; function EnumUsers(const Server: WideString; filter: DWORD; Callback: TEnumUers; Data: Pointer): DWORD; var ui1 : Pointer; pWork : Pointer; EntriesRead : DWORD; EntriesTotal : DWORD; NetError : DWORD; Loop : Integer; begin ui1 := nil; pWork := nil; try NetError := NetUserEnum(PWideChar(Server), 0, filter, ui1, MAX_PREFERRED_LENGTH, EntriesRead, EntriesTotal, nil); if (NetError = NERR_SUCCESS) and (EntriesRead > 0) then begin pWork := ui1; if Assigned(Callback) then begin for Loop := 0 to EntriesRead - 1 do begin if not Callback(PUserInfo1(ui1)^.usri1_name, EntriesRead, Data) then break; Inc(Integer(ui1), sizeof(Pointer)); end; end; end; finally NetApiBufferFree(pWork); end; Result := NetError; end; procedure TForm1.Button1Click(Sender: TObject); var Data: Pointer; begin Data := nil; EnumUsers('', 0, @EnumUsersCallback, Form1.Listbox1); end; end. ![]() Wenn du meine Unit benutzt, dann brauchst du natürlich nur die Callback implementieren und die Funktion EnumUsers aufrufen. |
AW: NetUserEnum die 2.
@Luckie: Ein Level-1-Info-Record, aber ebenfalls Level 0 als Abfrageparameter?
|
AW: NetUserEnum die 2.
Tatsache. Aber es funktioniert - zumindest für den Benutzernamen. Aber wenn ich da 1 einsetze bekomme ich eine AccessViolation. :gruebel: Den ersten Benutzzernamen bekomme ich noch, bei den folgen denn ist das Feld für den Benutzernamen nil. :shock:
|
AW: NetUserEnum die 2.
Du gehst in dem Code auch jeweils einen String weiter (
Delphi-Quellcode:
).
Inc(Integer(ui1), sizeof(Pointer));
Der InfoLevel 0 paßt also zum Inc, weil da ja auch nur die Benutzernamen hintereinanderstehen (was Anderes ließt du in dem Beispiel nicht aus, sonst würde es da wohl auch nicht ganz stimmen). MiKaEr geht statt Inc über ein Array, was einem
Delphi-Quellcode:
entsprechen würde.
Inc(Integer(...), sizeof(TUserInfo0));
PS: Integer und 64 Bit funktioniet auch nicht mehr, falls man das auch noch ausprobieren will. (wegen em Trottel, welcher den Integer eingefroren hat) |
AW: NetUserEnum die 2.
OK, wie müsste ich den Code ändern, damit er richtig funktioniert? Ich habe ihn eben mal debuggt, bin aber auf keinen grünen Zweig gekommen.
|
AW: NetUserEnum die 2.
War noch im editieren. :oops:
Du müßtest auch noch das Inc an den Record anpassen. (die Daten/Records liegen wohl hintereinander im Speicher) Falls eine Pointerarithmetik für den Record vorhanden ist, dann köndest du das INC weglassen und über
Delphi-Quellcode:
drauf zugreifen.
PUserInfo1(ui1)[index].usri1_name
Eventuell kann man auch gleich das ui1 als PUserInfo1 deklarieren. (
Delphi-Quellcode:
)
ui1[index].usri1_name
Womit dann Delphi, direkt beim Aufruf, jeweils ein implizites INC einbaut. |
AW: NetUserEnum die 2.
Jja, das habe ich auch schon überlegt. Ich habe es jetzt so:
Delphi-Quellcode:
Aber dann bekomme ich fünf mal den Admin. Das ist das erste Konto von fünf bei mir. Kommentiere ich es nicht aus, bekomme ich nur den ersten Benutzer und die folgenden sind nil. Und dann haut es mir den Code um die Ohren. :(
function EnumUsers(const Server: WideString; filter: DWORD; Callback: TEnumUers; Data: Pointer): DWORD;
var ui1 : PUserInfo1; pWork : Pointer; EntriesRead : DWORD; EntriesTotal : DWORD; NetError : DWORD; Loop : Integer; begin ui1 := nil; pWork := nil; try NetError := NetUserEnum(PWideChar(Server), 1, filter, ui1, MAX_PREFERRED_LENGTH, EntriesRead, EntriesTotal, nil); if (NetError = NERR_SUCCESS) and (EntriesRead > 0) then begin pWork := ui1; if Assigned(Callback) then begin for Loop := 0 to EntriesRead - 1 do begin if not Callback(PUserInfo1(ui1)^.usri1_name, EntriesRead, Data) then break; //Inc(Integer(ui1), sizeof(PUserInfo1)); end; end; end; finally NetApiBufferFree(pWork); end; Result := NetError; end; |
AW: NetUserEnum die 2.
Delphi-Quellcode:
oder
for Loop := 0 to EntriesRead - 1 do
if not Callback(PUserInfo1(ui1)[Loop].usri1_name, EntriesRead, Data) then break;
Delphi-Quellcode:
PS: Ich würde den Callback noch so anpassen, daß man dort das ganze TUserInfo1 anstatt nur des Usermames übergibt. (damit nichts umsonst ausgelesen wird)
for Loop := 0 to EntriesRead - 1 do
begin if not Callback(PUserInfo1(ui1)^.usri1_name, EntriesRead, Data) then break; Inc(NativeUInt(ui1), sizeof(TUserInfo1)); // T statt P und Integer, falls NativeUInt noch nicht geht end; |
AW: NetUserEnum die 2.
Perfekt. Die zweite Möglichkeit funktioniert. Hm, dafür habe ich andere Funktionen. Und davon mal abgesehen, verwend eich im Usermanager andere Routinen. ;)
|
Alle Zeitangaben in WEZ +1. Es ist jetzt 05:53 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz