(**************************************************************************)
(*                                                                        *)
(*  Setup for Weasel mail server                                          *)
(*  Copyright (C) 2025   Peter Moylan                                     *)
(*                                                                        *)
(*  This program is free software: you can redistribute it and/or modify  *)
(*  it under the terms of the GNU General Public License as published by  *)
(*  the Free Software Foundation, either version 3 of the License, or     *)
(*  (at your option) any later version.                                   *)
(*                                                                        *)
(*  This program is distributed in the hope that it will be useful,       *)
(*  but WITHOUT ANY WARRANTY; without even the implied warranty of        *)
(*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *)
(*  GNU General Public License for more details.                          *)
(*                                                                        *)
(*  You should have received a copy of the GNU General Public License     *)
(*  along with this program.  If not, see <http://www.gnu.org/licenses/>. *)
(*                                                                        *)
(*  To contact author:   http://www.pmoylan.org   peter@pmoylan.org       *)
(*                                                                        *)
(**************************************************************************)

IMPLEMENTATION MODULE HostLists;

        (****************************************************************)
        (*                                                              *)
        (*                      PM Setup for Weasel                     *)
        (*               Host list pages of the notebook                *)
        (*                                                              *)
        (*        Started:        8 July 1999                           *)
        (*        Last edited:    17 December 2025                      *)
        (*        Status:         OK                                    *)
        (*                                                              *)
        (****************************************************************)


FROM SYSTEM IMPORT CARD16, ADDRESS, CAST, ADR;

IMPORT OS2, OS2RTL, DID, Strings, CommonSettings, OneLine;

FROM Languages IMPORT
    (* type *)  LangHandle,
    (* proc *)  StrToBuffer, StrToBufferN;

FROM WSUINI IMPORT
    (* proc *)  OpenINIFile, CloseINIFile;

FROM RINIData IMPORT
    (* type *)  StringReadState,
    (* proc *)  INIFetch, INIPut, INIPutBinary,
                GetStringList, NextString, CloseStringList;

FROM MiscFuncs IMPORT
    (* type *)  CharArrayPointer,
    (* proc *)  EVAL, StringMatch;

FROM Inet2Misc IMPORT
    (* proc *)  Swap4, IPToString, NameIsNumeric, StringToIP;

FROM Misc IMPORT
    (* type *)  HostCategory,
    (* proc *)  WinSetDlgItemCard, WinQueryDlgItemCard;

FROM Sockets IMPORT
    (* const*)  AF_INET, SOCK_RAW, SOCK_DGRAM, AF_UNSPEC, SIOSTATAT,
                IFMIB_ENTRIES, NotASocket,
    (* type *)  Socket, iftype,
    (* proc *)  socket, ioctl, os2_ioctl, soclose;

FROM NetIF IMPORT
    (* const*)  IFNAMSIZ,
    (* type *)  ifconf, ifreq;

FROM ioctl IMPORT
    (* const*)  SIOCGIFCONF;

FROM LowLevel IMPORT
    (* proc *)  AddOffset, LS, IAND;

FROM Storage IMPORT
    (* proc *)  ALLOCATE, DEALLOCATE;

(**************************************************************************)

CONST
    Nul = CHR(0);  space = ' ';  tab = CHR(9);
    NameLength = 256;

TYPE
    LabelString = ARRAY [0..63] OF CHAR;
    Label = ARRAY HostCategory OF LabelString;
    IDarray = ARRAY HostCategory OF CARDINAL;
    CreationRecord = RECORD
                         size: CARD16;
                         cat : HostCategory;
                     END (*RECORD*);

CONST
    INILabel = Label {"Local", "nochunking", "Whitelisted", "MayRelay", "RelayDest", "Banned"};
    PageName = Label {"Local", "Chunking", "Whitelist", "Trusted", "GateFor", "Banned"};
    DialogueID = IDarray {DID.LocalHostNames, DID.Chunking, DID.WhitelistPage, DID.RelaySources,
                                    DID.RelayDest, DID.BannedHosts};
    HostList = IDarray {DID.localhostlist, DID.nochunkinglist, DID.whitelist, DID.mayrelaylist,
                                    DID.relaydestlist, DID.bannedlist};
    HLLabel = IDarray {DID.locallistlabel, DID.nochunkinglabel, DID.whitelistlabel, DID.mayrelaylistlabel,
                                 DID.relaydestlistlabel, DID.bannedlistlabel};
    HLExplain = IDarray {DID.locallistexplain, DID.nochunkexplain, DID.whitelistexplain, DID.mayrelaylistexplain,
                                 DID.relaydestlistexplain, DID.bannedlistexplain};
    HLCount = IDarray {DID.locallistcount, DID.nochunkcount, DID.whitelistcount, DID.mayrelaylistcount,
                                 DID.relaydestlistcount, DID.bannedlistcount};
    AddButton = IDarray {DID.AddLocalHostName, DID.AddNoChunk, DID.AddWhitelistName, DID.AddMayRelayName,
                            DID.AddRelayDestName, DID.AddBannedName};
    EditButton = IDarray {DID.EditLocalHostName, DID.EditNoChunk, DID.EditWhitelistName, DID.EditMayRelayName,
                            DID.EditRelayDestName, DID.EditBannedName};
    DeleteButton = IDarray {DID.DeleteLocalHostName, DID.DeleteNoChunk, DID.DeleteWhitelistName, DID.DeleteMayRelayName,
                            DID.DeleteRelayDestName, DID.DeleteBannedName};
    PromoteButton = IDarray {DID.PromoteLocalHostName, DID.PromoteNoChunk, DID.PromoteWhitelistName, DID.PromoteMayRelayName,
                             DID.PromoteRelayDestName, DID.PromoteBannedName};

VAR
    OurLang: LangHandle;
    Count: ARRAY HostCategory OF CARDINAL;
    Handle, notebookhandle: ARRAY HostCategory OF OS2.HWND;
    PageActive, Changed: ARRAY HostCategory OF BOOLEAN;
    ChangeInProgress: ARRAY HostCategory OF BOOLEAN;
    Multidomain, UseTNI: BOOLEAN;
    PageID: ARRAY HostCategory OF CARDINAL;
    CreationData: CreationRecord;
    OurFontGroup: ARRAY HostCategory OF CommonSettings.FontGroup;

(************************************************************************)
(*                    OPERATIONS ON DIALOGUE LABELS                     *)
(************************************************************************)

PROCEDURE ShowCount (c: HostCategory);

    (* Updates the "count" display. *)

    VAR stringval: ARRAY [0..511] OF CHAR;

    BEGIN
        StrToBufferN (OurLang, "HostList.count", Count[c], stringval);
        OS2.WinSetDlgItemText (Handle[c], HLCount[c], stringval);
    END ShowCount;

(************************************************************************)

PROCEDURE SetLanguage (lang: LangHandle);

    (* Relabels all hostlist pages in the new language. *)

    VAR code: ARRAY [0..127] OF CHAR;
        stringval: ARRAY [0..511] OF CHAR;
        c: HostCategory;

    BEGIN
        OurLang := lang;
        FOR c := MIN(HostCategory) TO MAX(HostCategory) DO
            IF PageActive[c] THEN
                IF c = chunking THEN
                    StrToBuffer (lang, "Chunking.max.Label", stringval);
                    OS2.WinSetDlgItemText (Handle[c], DID.maxchunksizelabel, stringval);
                END (*IF*);
                Strings.Assign (PageName[c], code);
                Strings.Append (".tab", code);
                StrToBuffer (lang, code, stringval);
                OS2.WinSendMsg (notebookhandle[c], OS2.BKM_SETTABTEXT,
                                CAST(ADDRESS,PageID[c]), ADR(stringval));
                Strings.Assign (PageName[c], code);
                IF (c = local) AND Multidomain THEN
                    Strings.Append (".LabelMD", code);
                ELSE
                    Strings.Append (".Label", code);
                END (*IF*);
                StrToBuffer (lang, code, stringval);
                OS2.WinSetDlgItemText (Handle[c], HLLabel[c], stringval);
                Strings.Assign (PageName[c], code);
                Strings.Append (".explain", code);
                StrToBuffer (lang, code, stringval);
                OS2.WinSetDlgItemText (Handle[c], HLExplain[c], stringval);
                ShowCount (c);
            END (*IF*);
        END (*FOR*);

        (* Special cases for the Local page. *)

        StrToBuffer (lang, "Local.AddAll", stringval);
        OS2.WinSetDlgItemText (Handle[local], DID.AddLocalAddresses, stringval);

        StrToBuffer (lang, "Local.StrictChecking", stringval);
        OS2.WinSetDlgItemText (Handle[local], DID.StrictChecking, stringval);

        (* Buttons. *)

        StrToBuffer (lang, "Buttons.Add", stringval);
        FOR c := MIN(HostCategory) TO MAX(HostCategory) DO
            IF PageActive[c] THEN
                OS2.WinSetDlgItemText (Handle[c], AddButton[c], stringval);
            END (*IF*);
        END (*FOR*);

        StrToBuffer (lang, "Buttons.Edit", stringval);
        FOR c := MIN(HostCategory) TO MAX(HostCategory) DO
            IF PageActive[c] THEN
                OS2.WinSetDlgItemText (Handle[c], EditButton[c], stringval);
            END (*IF*);
        END (*FOR*);

        StrToBuffer (lang, "Buttons.Promote", stringval);
        FOR c := MIN(HostCategory) TO MAX(HostCategory) DO
            IF PageActive[c] THEN
                OS2.WinSetDlgItemText (Handle[c], PromoteButton[c], stringval);
            END (*IF*);
        END (*FOR*);

        StrToBuffer (lang, "Buttons.Delete", stringval);
        FOR c := MIN(HostCategory) TO MAX(HostCategory) DO
            IF PageActive[c] THEN
                OS2.WinSetDlgItemText (Handle[c], DeleteButton[c], stringval);
            END (*IF*);
        END (*FOR*);

    END SetLanguage;

(************************************************************************)
(*                    CONVERSION TO A STANDARD FORM                     *)
(************************************************************************)

PROCEDURE NormaliseCIDRentry (VAR (*INOUT*) entry: ARRAY OF CHAR): BOOLEAN;

    (* If entry has CIDR format, adjusts the base address.  Otherwise   *)
    (* leaves entry unchanged.  Returns TRUE if entry changed.          *)

    (********************************************************************)

    PROCEDURE BitsToMask (N: CARDINAL): CARDINAL;

        (* Produces a big-endian number where the initial N bits are 1  *)
        (* and the remaining bits are 0.                                *)

        CONST AllOnes = 0FFFFFFFFH;

        VAR mask: CARDINAL;

        BEGIN
            IF N = 0 THEN
                RETURN 0;
            END (*IF*);
            IF N >= 32 THEN
                mask := AllOnes;
            ELSE
                mask := LS(AllOnes, 32-N);
            END (*IF*);
            RETURN Swap4(mask);
        END BitsToMask;

    (********************************************************************)

    VAR pos, IPaddr, nbits: CARDINAL;
        HaveSlash, changed: BOOLEAN;
        ch: CHAR;
        tail: ARRAY [0..511] OF CHAR;

    BEGIN
        changed := FALSE;

        (* Separate out trailing '/' and number if present. *)

        Strings.FindNext ('/', entry, 0, HaveSlash, pos);
        IF HaveSlash THEN
            Strings.Assign (entry, tail);
            entry[pos] := Nul;
            IF pos > 0 THEN
                Strings.Delete (tail, 0, pos);
            END (*IF*);
        END (*IF*);

        IF HaveSlash AND NameIsNumeric(entry) THEN

            (* This is the case where we do the modification.  We don't *)
            (* do a legality check on what follows the slash, because   *)
            (* there is no legal case where a slash can be followed by  *)
            (* something that is not a number.                          *)

            pos := 1;  nbits := 0;
            ch := tail[pos];
            WHILE (ch >= '0') AND (ch <= '9') DO
                nbits := 10*nbits + ORD(ch) - ORD('0');
                INC (pos);
                ch := tail[pos];
            END (*WHILE*);

            IPaddr := IAND (StringToIP(entry), BitsToMask(nbits));
            IPToString (IPaddr, FALSE, entry);
            changed := TRUE;

        END (*IF*);

        (* Reinsert what we have removed. *)

        IF HaveSlash THEN
            Strings.Append (tail, entry);
        END (*IF*);

        RETURN changed;

    END NormaliseCIDRentry;

(************************************************************************)

PROCEDURE CommentStart (VAR (*IN*) entry: ARRAY OF CHAR): CARDINAL;

    (* Returns the starting position of a comment in entry.  If no      *)
    (* comment, returns a value beyond the end of the string.           *)

    VAR first, pos: CARDINAL;
        tabstr: ARRAY [0..0] OF CHAR;
        found: BOOLEAN;

    BEGIN
        first := MAX(CARDINAL);
        tabstr[0] := tab;
        Strings.FindNext (space, entry, 0, found, pos);
        IF found AND (pos < first) THEN first := pos END (*IF*);
        Strings.FindNext (tabstr, entry, 0, found, pos);
        IF found AND (pos < first) THEN first := pos END (*IF*);
        Strings.FindNext (';', entry, 0, found, pos);
        IF found AND (pos < first) THEN first := pos END (*IF*);
        Strings.FindNext ('#', entry, 0, found, pos);
        IF found AND (pos < first) THEN first := pos END (*IF*);
        RETURN first;
    END CommentStart;

(************************************************************************)

PROCEDURE SeparateComment (VAR (*INOUT*) entry: ARRAY OF CHAR;
                                VAR (*OUT*) comment: ARRAY OF CHAR);

    (* Removes any comment from entry, but saves it so that it can  *)
    (* later be reinserted.                                         *)

    VAR L, pos: CARDINAL;

    BEGIN
        L := Strings.Length (entry);
        pos := CommentStart (entry);
        IF pos >= L THEN
            comment[0] := Nul;
        ELSE
            Strings.Extract (entry, pos, L - pos, comment);
            entry[pos] := Nul;
        END (*IF*);
    END SeparateComment;

(************************************************************************)

PROCEDURE StripLeadingWhitespace (VAR (*INOUT*) entry: ARRAY OF CHAR): BOOLEAN;

    (* Removes leading spaces and tabs.  Returns TRUE iff entry has changed. *)

    VAR changed: BOOLEAN;

    BEGIN
        changed := FALSE;
        WHILE (entry[0] = space) OR (entry[0] = tab) DO
            Strings.Delete (entry, 0, 1);
            changed := TRUE;
        END (*WHILE*);
        RETURN changed;
    END StripLeadingWhitespace;

(************************************************************************)

PROCEDURE NormaliseEntry (VAR (*INOUT*) entry: ARRAY OF CHAR): BOOLEAN;

    (* Changes obsolete or nonstandard entries to a standard form.  *)
    (* This includes removing leading whitespace, and removing an   *)
    (* obsolete leading CIDR if present.                            *)
    (* Returns TRUE if we have changed the entry.                   *)

    VAR comment: ARRAY [0..127] OF CHAR;
        changed, found, bracketed: BOOLEAN;
        L, pos: CARDINAL;

    BEGIN
        changed := StripLeadingWhitespace (entry);
        Strings.FindNext ("CIDR", entry, 0, found, pos);
        IF found AND (pos = 0) THEN
            Strings.Delete (entry, 0, 4);
            EVAL (StripLeadingWhitespace (entry));
            changed := TRUE;
        END (*IF*);
        SeparateComment (entry, comment);
        bracketed := entry[0] = '[';
        IF bracketed THEN
            Strings.Delete (entry, 0, 1);
            L := Strings.Length(entry)-1;
            IF entry[L] = ']' THEN
                entry[L] := Nul;
            END (*IF*);
        END (*IF*);
        IF entry[0] = '.' THEN
            Strings.Insert ('*', 0, entry);
            changed := TRUE;
        ELSIF NOT changed THEN
            changed := NormaliseCIDRentry (entry);
        END (*IF*);
        IF bracketed THEN
            Strings.Insert ('[', 0, entry);
            Strings.Append (']', entry);
        END (*IF*);
        IF comment[0] <> Nul THEN
            Strings.Append (comment, entry);
        END (*IF*);
        RETURN changed;
    END NormaliseEntry;

(************************************************************************)
(*                   CHECKING ENTRIES FOR DUPLICATES                    *)
(************************************************************************)

PROCEDURE IsDuplicate (VAR (*IN*) name: ARRAY OF CHAR;
                                     bufptr: CharArrayPointer): BOOLEAN;

    (* bufptr points to a string of strings.  We return TRUE iff name   *)
    (* duplicates one of those strings.                                 *)

    (* Note that we remove comments before comparing. *)

    VAR namecopy, entry: ARRAY [0..NameLength] OF CHAR;
        comment: ARRAY [0..127] OF CHAR;
        j, k, nameval: CARDINAL;  ch: CHAR;
        nameisnumeric: BOOLEAN;

    BEGIN
        Strings.Assign (name, namecopy);
        SeparateComment (namecopy, comment);
        nameisnumeric := NameIsNumeric (namecopy);
        nameval := 0;       (* to avoid a compiler warning *)
        IF nameisnumeric THEN
            nameval := StringToIP (namecopy);
        END (*IF*);

        j := 0;
        WHILE bufptr^[j] <> Nul DO

            (* Pick up next entry. *)

            k := 0;
            REPEAT
                ch := bufptr^[j];
                entry[k] := ch;
                INC (j);  INC (k);
            UNTIL ch = Nul;
            SeparateComment (entry, comment);

            (* Do the comparison. *)

            IF NameIsNumeric(entry) = nameisnumeric THEN
                IF nameisnumeric THEN
                    IF StringToIP(entry) = nameval THEN
                        RETURN TRUE;
                    END (*IF*);
                ELSIF StringMatch (namecopy, entry) THEN
                    RETURN TRUE;
                END (*IF*);
            END (*IF*);
        END (*WHILE*);

        RETURN FALSE;

    END IsDuplicate;

(************************************************************************)
(*                 MOVING DATA TO AND FROM THE INI FILE                 *)
(************************************************************************)

PROCEDURE StoreList (category: HostCategory;  hwnd: OS2.HWND);

    (* Stores a HostList into the INI file.  The hwnd parameter is the  *)
    (* handle of the listbox.  We assume that the INI file is already   *)
    (* open.                                                            *)

    VAR bufptr: CharArrayPointer;
        BufferSize, ActualSize: CARDINAL;
        j, k, count, index: CARDINAL;
        name: ARRAY [0..NameLength-1] OF CHAR;

    BEGIN
        (* Work out how much buffer space we need. *)

        BufferSize := 0;
        count := OS2.ULONGFROMMR(OS2.WinSendMsg (hwnd, OS2.LM_QUERYITEMCOUNT, NIL, NIL));
        IF count > 0 THEN
            FOR index := 0 TO count-1 DO
                INC (BufferSize,
                     OS2.ULONGFROMMR(OS2.WinSendMsg (hwnd, OS2.LM_QUERYITEMTEXTLENGTH,
                                   OS2.MPFROMUSHORT(index), NIL)) + 1);
            END (*FOR*);
        END (*IF*);

        (* Create the string buffer. *)

        IF BufferSize = 0 THEN
            bufptr := NIL;
        ELSE
            INC (BufferSize);
            ALLOCATE (bufptr, BufferSize);
            bufptr^[0] := Nul;
        END (*IF*);
        ActualSize := BufferSize;

        (* Store all the strings into the buffer. *)

        IF count > 0 THEN
            j := 0;
            FOR index := 0 TO count-1 DO
                OS2.WinSendMsg (hwnd, OS2.LM_QUERYITEMTEXT,
                                OS2.MPFROM2USHORT(index, NameLength), ADR(name));
                IF IsDuplicate (name, bufptr) THEN
                    DEC (ActualSize, Strings.Length(name)+1);
                ELSE
                    k := 0;
                    REPEAT
                        bufptr^[j] := name[k];
                        INC (k);  INC (j);
                    UNTIL (name[k] = Nul) OR (k = NameLength);
                    bufptr^[j] := Nul;
                    INC (j);
                    bufptr^[j] := Nul;

                    (* That last Nul will be overwritten by the next    *)
                    (* entry, except when there isn't a next one.       *)

                END (*IF*);
            END (*FOR*);

        END (*IF*);

        (* Write the buffer to the INI file. *)

        IF BufferSize = 0 THEN
            INIPutBinary ("$SYS", INILabel[category], j, 0);
        ELSE
            INIPutBinary ("$SYS", INILabel[category], bufptr^, ActualSize);
        END (*IF*);

        (* Deallocate the buffer space. *)

        IF BufferSize > 0 THEN
            DEALLOCATE (bufptr, BufferSize);
        END (*IF*);

    END StoreList;

(************************************************************************)

PROCEDURE LoadValues (category: HostCategory;  hwnd: OS2.HWND);

    (* Fills the dialogue elements on the user page with data from the  *)
    (* INI file, or loads default values if they're not in the INI file.*)

    VAR name: ARRAY [0..NameLength-1] OF CHAR;
        state: StringReadState;  size: CARDINAL;  flag: BOOLEAN;

    BEGIN

        (* TEMPORARY ARRANGEMENT: SET THE "Changed" FLAG    *)
        (* UNCONDITIONALLY.                                 *)

                    Changed[category] := TRUE;

        Count[category] := 0;
        OpenINIFile;

        (* Load a hostname list from the INI file. *)

        GetStringList ("$SYS", INILabel[category], state);
        REPEAT
            NextString (state, name);
            IF name[0] <> Nul THEN

                Changed[category] := NormaliseEntry (name) OR Changed[category];

                (* Add name to the listbox. *)

                OS2.WinSendDlgItemMsg (hwnd, HostList[category], OS2.LM_INSERTITEM,
                     OS2.MPFROMSHORT(OS2.LIT_END), ADR(name));
                INC (Count[category]);

            END (*IF*);
        UNTIL name[0] = Nul;
        CloseStringList (state);

        (* The local "strict checking" checkbox, and the chunking special field. *)

        IF category = local THEN
            IF NOT INIFetch ("$SYS", "StrictChecking", flag) THEN
                flag := FALSE;
            END (*IF*);
            OS2.WinSendDlgItemMsg (hwnd, DID.StrictChecking, OS2.BM_SETCHECK,
                                     OS2.MPFROMSHORT(ORD(flag)), NIL);
        END (*IF*);

        (* Special case for the chunking page. *)

        IF category = chunking THEN
            IF NOT INIFetch ("$SYS", "maxchunksize", size) THEN
                size := 128;
            END (*IF*);
            WinSetDlgItemCard (hwnd, DID.maxchunksize, size);
        END (*IF*);

        CloseINIFile;

        ShowCount (category);

    END LoadValues;

(************************************************************************)

PROCEDURE AddLocalAddresses (hwnd: OS2.HWND;  category: HostCategory);

    (* Adds all local IP addresses to the listbox. *)

    (********************************************************************)

    PROCEDURE AddressExists (addr: CARDINAL): BOOLEAN;

        (* Returns TRUE iff the listbox already contains the numeric    *)
        (* address addr.                                                *)

        VAR k, count: CARDINAL;
            name: ARRAY [0..NameLength-1] OF CHAR;

        BEGIN
            count := OS2.ULONGFROMMR(OS2.WinSendDlgItemMsg (hwnd, DID.localhostlist,
                                        OS2.LM_QUERYITEMCOUNT, NIL, NIL));
            k := 0;
            LOOP
                IF k >= count THEN
                    RETURN FALSE;
                END (*IF*);
                OS2.WinSendDlgItemMsg (hwnd, DID.localhostlist, OS2.LM_QUERYITEMTEXT,
                             OS2.MPFROM2USHORT(k, NameLength), ADR(name));
                IF NameIsNumeric(name) AND (StringToIP(name) = addr) THEN
                    RETURN TRUE;
                END (*IF*);
                INC (k);
            END (*LOOP*);
        END AddressExists;

    (********************************************************************)

    PROCEDURE AddAddress (addr: CARDINAL);

        (* Adds one address to the listbox, unless it's a duplicate. *)

        VAR text: ARRAY [0..19] OF CHAR;

        BEGIN
            IF NOT AddressExists(addr) THEN
                IPToString (addr, TRUE, text);

                (* Add text to the listbox. *)

                OS2.WinSendDlgItemMsg (hwnd, DID.localhostlist, OS2.LM_INSERTITEM,
                     OS2.MPFROMSHORT(OS2.LIT_END), ADR(text));
                INC (Count[category]);
                ShowCount (category);

            END (*IF*);

        END AddAddress;

    (********************************************************************)

    TYPE ifreqPtr = POINTER TO ifreq;

    VAR s: Socket;  i, total: CARDINAL;
        ifc: ifconf;
        ifr: ifreqPtr;
        buf: ARRAY [0..IFMIB_ENTRIES*SIZE(ifreq)-1] OF CHAR;

    BEGIN
        (* Use an ioctl call to read all the ifreq records      *)
        (* into buffer "buf", and set "total" to the number of  *)
        (* such records.                                        *)

        s := socket(AF_INET, SOCK_DGRAM, AF_UNSPEC);
        IF s = NotASocket THEN
            total := 0;
        ELSE
            ifc.ifc_len := SIZE(buf);
            ifc.ifcu_buf := ADR(buf);
            IF ioctl (s, SIOCGIFCONF, ifc, SIZE(ifc)) < 0 THEN
                total := 0;
            ELSE
                total := ifc.ifc_len DIV SIZE(ifreq);
            END (*IF*);
            soclose(s);
        END (*IF*);

        (* Now work out which of the interface records are for  *)
        (* active interfaces, and put those addresses into the  *)
        (* listbox (unless they are duplicates).                *)

        IF total > 0 THEN

            ifr := ADR(buf);
            FOR i := 0 TO total-1 DO

                IF ifr^.ifru_addr.family = AF_INET THEN
                    AddAddress (ifr^.ifru_addr.in_addr.addr);
                END (*IF*);
                ifr := AddOffset (ifr, SIZE(ifreq));

            END (*FOR*);

        END (* IF total > 0 *);

    END AddLocalAddresses;

(************************************************************************)

PROCEDURE NormaliseAllEntries (j: HostCategory);

    (* Converts, if necessary, all entries for this page to a standard form. *)

    VAR w: OS2.HWND;
        index, count: CARDINAL;
        name: ARRAY [0..NameLength-1] OF CHAR;

    BEGIN
        w := OS2.WinWindowFromID(Handle[j],HostList[j]);
        count := OS2.ULONGFROMMR(OS2.WinSendMsg (w, OS2.LM_QUERYITEMCOUNT, NIL, NIL));

        (* Check each entry to see whether it needs updating. *)

        IF count > 0 THEN
            FOR index := 0 TO count-1 DO
                OS2.WinSendMsg (w, OS2.LM_QUERYITEMTEXT,
                                OS2.MPFROM2USHORT(index, NameLength), ADR(name));
                IF NormaliseEntry(name) THEN
                    OS2.WinSendMsg (w, OS2.LM_SETITEMTEXT,
                                OS2.MPFROMSHORT(index), ADR(name));
                    Changed[j] := TRUE;
                END (*IF*);
            END (*FOR*);
        END (*IF*);

    END NormaliseAllEntries;

(************************************************************************)

PROCEDURE StoreData (j: HostCategory);

    (* Stores back data for this page. *)

    VAR w: OS2.HWND;  count: CARDINAL;  size: CARDINAL;  bool: BOOLEAN;

    BEGIN
        IF (j = local) AND Multidomain THEN
            w := OS2.WinWindowFromID(Handle[j],HostList[j]);
            count := OS2.ULONGFROMMR(OS2.WinSendMsg (w, OS2.LM_QUERYITEMCOUNT, NIL, NIL));
            IF count = 0 THEN
                AddLocalAddresses(Handle[j], j);
                Changed[j] := TRUE;
            END (*IF*);
        END (*IF*);
        NormaliseAllEntries (j);
        OpenINIFile;
        IF Changed[j] THEN
            StoreList(j, OS2.WinWindowFromID(Handle[j],HostList[j]));
        END (*IF*);
        Changed[j] := FALSE;

        IF j = local THEN

            (* Also save the local "strict checking" checkbox state. *)

            bool := OS2.LONGFROMMR (OS2.WinSendDlgItemMsg (Handle[j],
                                             DID.StrictChecking,
                                             OS2.BM_QUERYCHECK, NIL, NIL)) > 0;
            INIPut ("$SYS", "StrictChecking", bool);
        END (*IF*);

        (* Special case for the chunking page. *)

        IF j = chunking THEN
            WinQueryDlgItemCard (Handle[j], DID.maxchunksize, size);
            INIPut ("$SYS", "maxchunksize", size);
        END (*IF*);

        CloseINIFile;
    END StoreData;

(**************************************************************************)

PROCEDURE ["SysCall"] DialogueProc(hwnd     : OS2.HWND
                     ;msg      : OS2.ULONG
                     ;mp1, mp2 : OS2.MPARAM): OS2.MRESULT;

    VAR ButtonID, NotificationCode: CARDINAL;
        index: INTEGER;
        listwindow: OS2.HWND;
        name: ARRAY [0..NameLength-1] OF CHAR;
        message: ARRAY [0..127] OF CHAR;
        category: HostCategory;
        p: POINTER TO CreationRecord;

    BEGIN

        IF msg = OS2.WM_INITDLG THEN
            p := mp2;
            category := p^.cat;
            OS2.WinSetWindowPos (hwnd, 0, 0, 0, 0, 0, OS2.SWP_MOVE);
            OS2.WinEnableWindow (OS2.WinWindowFromID(hwnd, EditButton[category]), FALSE);
            OS2.WinEnableWindow (OS2.WinWindowFromID(hwnd, PromoteButton[category]), FALSE);
            OS2.WinEnableWindow (OS2.WinWindowFromID(hwnd, DeleteButton[category]), FALSE);
            IF category = local THEN
                IF Multidomain THEN
                    OS2.WinShowWindow (OS2.WinWindowFromID(hwnd, DID.locallistexplain), FALSE);
                    OS2.WinShowWindow (OS2.WinWindowFromID(hwnd, DID.StrictChecking), TRUE);
                ELSE
                    OS2.WinShowWindow (OS2.WinWindowFromID(hwnd, DID.locallistexplain), TRUE);
                    OS2.WinShowWindow (OS2.WinWindowFromID(hwnd, DID.StrictChecking), FALSE);
                END (*IF*);
            END (*IF*);
            LoadValues (category, hwnd);
            PageActive[category] := TRUE;
            RETURN NIL;
        END (*IF*);

        (* Identify which page we're dealing with. *)

        category := MIN(HostCategory);
        LOOP
            IF Handle[category] = hwnd THEN
                EXIT (*LOOP*);
            ELSIF category = MAX(HostCategory) THEN
                RETURN OS2.WinDefDlgProc(hwnd, msg, mp1, mp2);
            ELSE
                INC (category);
            END (*IF*);
        END (*LOOP*);

        index := OS2.LONGFROMMR(
                   OS2.WinSendDlgItemMsg (hwnd, HostList[category], OS2.LM_QUERYSELECTION, NIL, NIL));
        IF msg = OS2.WM_COMMAND THEN

            listwindow := OS2.WinWindowFromID(hwnd,HostList[category]);
            ButtonID := OS2.SHORT1FROMMP(mp1);

            IF ButtonID = AddButton[category] THEN
                   name := "";
                   StrToBuffer (OurLang, "Local.EnterName", message);
                   OneLine.Edit (hwnd, message, name, UseTNI);
                   Changed[category] := NormaliseEntry (name);
                   IF name[0] <> Nul THEN
                       IF index = OS2.LIT_NONE THEN
                           index := 0;
                       ELSE
                           INC(index);
                       END (*IF*);
                       OS2.WinSendDlgItemMsg (hwnd, HostList[category], OS2.LM_INSERTITEM,
                              OS2.MPFROMSHORT(index), ADR(name));
                       OS2.WinSendDlgItemMsg (hwnd, HostList[category], OS2.LM_SELECTITEM,
                              OS2.MPFROMSHORT(index), OS2.MPFROMSHORT(ORD(TRUE)));
                   END (*IF*);
                   INC (Count[category]);
                   ShowCount (category);
                   Changed[category] := TRUE;

            ELSIF ButtonID = EditButton[category] THEN
                   OS2.WinSendMsg (listwindow, OS2.LM_QUERYITEMTEXT,
                            OS2.MPFROM2USHORT(index, NameLength), ADR(name));
                   StrToBuffer (OurLang, "Local.EnterName", message);
                   OneLine.Edit (hwnd, message, name, UseTNI);
                   Changed[category] := NormaliseEntry (name);
                   IF name[0] <> Nul THEN
                       OS2.WinSendDlgItemMsg (hwnd, HostList[category], OS2.LM_SETITEMTEXT,
                              OS2.MPFROMSHORT(index), ADR(name));
                       OS2.WinSendDlgItemMsg (hwnd, HostList[category], OS2.LM_SELECTITEM,
                              OS2.MPFROMSHORT(index), OS2.MPFROMSHORT(ORD(TRUE)));
                   END (*IF*);
                   Changed[category] := TRUE;

            ELSIF ButtonID = PromoteButton[category] THEN

                   OS2.WinSendMsg (listwindow, OS2.LM_QUERYITEMTEXT,
                                   OS2.MPFROM2USHORT(index, NameLength), ADR(name));
                   OS2.WinSendMsg (listwindow, OS2.LM_DELETEITEM,
                                          OS2.MPFROMSHORT(index), NIL);
                   DEC (index);
                   OS2.WinSendMsg (listwindow, OS2.LM_INSERTITEM,
                          OS2.MPFROMSHORT(index), ADR(name));
                   OS2.WinSendMsg (listwindow, OS2.LM_SELECTITEM,
                          OS2.MPFROMSHORT(index), OS2.MPFROMSHORT(ORD(TRUE)));
                   Changed[category] := TRUE;

            ELSIF ButtonID = DeleteButton[category] THEN

                   OS2.WinSendDlgItemMsg (hwnd, HostList[category], OS2.LM_DELETEITEM,
                                          OS2.MPFROMSHORT(index), NIL);
                   OS2.WinEnableWindow (OS2.WinWindowFromID(hwnd, EditButton[category]), FALSE);
                   OS2.WinEnableWindow (OS2.WinWindowFromID(hwnd, PromoteButton[category]), FALSE);
                   OS2.WinEnableWindow (OS2.WinWindowFromID(hwnd, DeleteButton[category]), FALSE);
                   DEC (Count[category]);
                   ShowCount (category);
                   Changed[category] := TRUE;

            ELSIF ButtonID = DID.AddLocalAddresses THEN

                   AddLocalAddresses(hwnd, category);
                   Changed[category] := TRUE;

            END (*IF*);
            RETURN NIL;

        ELSIF msg = OS2.WM_PRESPARAMCHANGED THEN

            IF ChangeInProgress[category] THEN
                RETURN OS2.WinDefDlgProc(hwnd, msg, mp1, mp2);
            ELSE
                ChangeInProgress[category] := TRUE;
                CommonSettings.UpdateFontFrom (hwnd, OurFontGroup[category]);
                ChangeInProgress[category] := FALSE;
                RETURN NIL;
            END (*IF*);

        ELSIF msg = OS2.WM_CONTROL THEN

            NotificationCode := OS2.ULONGFROMMP(mp1);
            ButtonID := NotificationCode MOD 65536;
            NotificationCode := NotificationCode DIV 65536;
            IF ButtonID = HostList[category] THEN
                IF NotificationCode = OS2.LN_SELECT THEN

                    (* For some reason the more obvious code doesn't work below, so     *)
                    (* we have to use an if/then/else construct.                        *)

                    IF index > 0 THEN
                        OS2.WinEnableWindow (OS2.WinWindowFromID(hwnd, PromoteButton[category]), TRUE);
                    ELSE
                        OS2.WinEnableWindow (OS2.WinWindowFromID(hwnd, PromoteButton[category]), FALSE);
                    END (*IF*);
                    OS2.WinEnableWindow (OS2.WinWindowFromID(hwnd, EditButton[category]), TRUE);
                    OS2.WinEnableWindow (OS2.WinWindowFromID(hwnd, DeleteButton[category]), TRUE);
                    RETURN NIL;
                ELSE
                    RETURN OS2.WinDefDlgProc(hwnd, msg, mp1, mp2);
                END (*IF*);
            ELSE
                RETURN OS2.WinDefDlgProc(hwnd, msg, mp1, mp2);
            END (*IF*);

        ELSE
            RETURN OS2.WinDefDlgProc(hwnd, msg, mp1, mp2);
        END (*CASE*);
    END DialogueProc;

(**************************************************************************)

PROCEDURE CreatePage (notebook: OS2.HWND;  category: HostCategory;
                      AfterPage: CARDINAL;  group: CommonSettings.FontGroup;
                      ModeIsMultidomain, TNImode: BOOLEAN;
                      VAR (*OUT*) pageID: CARDINAL);

    (* Creates a host list page and adds it to the notebook. *)

    VAR pagehandle: OS2.HWND;
        code: ARRAY [0..127] OF CHAR;
        Label: LabelString;

    BEGIN
        UseTNI := TNImode;
        Multidomain := ModeIsMultidomain;
        notebookhandle[category] := notebook;
        OurFontGroup[category] := group;
        WITH CreationData DO
            size := SIZE(CreationRecord);
            cat  := category;
        END (*WITH*);
        Changed[category] := FALSE;
        pagehandle := OS2.WinLoadDlg(notebook, notebook,
                       DialogueProc,    (* dialogue procedure *)
                       0,                   (* use resources in EXE *)
                       DialogueID[category], (* dialogue ID *)
                       ADR(CreationData));                 (* creation parameters *)
        Handle[category] := pagehandle;
        IF AfterPage = 0 THEN
            PageID[category] := OS2.ULONGFROMMR (OS2.WinSendMsg (notebook, OS2.BKM_INSERTPAGE,
                         NIL,
                         OS2.MPFROM2SHORT (OS2.BKA_MAJOR+OS2.BKA_AUTOPAGESIZE, OS2.BKA_LAST)));
        ELSE
            PageID[category] := OS2.ULONGFROMMR (OS2.WinSendMsg (notebook, OS2.BKM_INSERTPAGE,
                         CAST (ADDRESS, AfterPage),
                         OS2.MPFROM2SHORT (OS2.BKA_MAJOR+OS2.BKA_AUTOPAGESIZE, OS2.BKA_NEXT)));
        END (*IF*);
        Strings.Assign (PageName[category], code);
        Strings.Append (".tab", code);
        StrToBuffer (OurLang, code, Label);
        OS2.WinSendMsg (notebook, OS2.BKM_SETTABTEXT,
                        CAST(ADDRESS,PageID[category]), ADR(Label));
        OS2.WinSendMsg (notebook, OS2.BKM_SETPAGEWINDOWHWND,
                        CAST(ADDRESS,PageID[category]), CAST(ADDRESS,pagehandle));
        pageID := PageID[category];
    END CreatePage;

(**************************************************************************)

PROCEDURE SetLocalFont (VAR (*IN*) name: CommonSettings.FontName);

    (* Sets the font of the text on the "Local" page. *)

    CONST bufsize = CommonSettings.FontNameSize;

    BEGIN
        OS2.WinSetPresParam (Handle[local], OS2.PP_FONTNAMESIZE, bufsize, name);
    END SetLocalFont;

(**************************************************************************)

PROCEDURE SetFonts (VAR (*IN*) name: CommonSettings.FontName);

    (* Sets the font of the text on all host pages except for the local page. *)

    CONST bufsize = CommonSettings.FontNameSize;

    VAR c: HostCategory;

    BEGIN
        FOR c := mayrelay TO MAX(HostCategory) DO
            OS2.WinSetPresParam (Handle[c], OS2.PP_FONTNAMESIZE, bufsize, name);
        END (*FOR*);
    END SetFonts;

(**************************************************************************)

PROCEDURE Close (notebook: OS2.HWND;  category: HostCategory);

    (* Shuts down this window and removes it from the notebook. *)

    BEGIN
        OurFontGroup[category] := CommonSettings.NilFontGroup;
        StoreData (category);
        PageActive[category] := FALSE;
        OS2.WinSendMsg (notebook, OS2.BKM_DELETEPAGE,
                        CAST(ADDRESS, PageID[category]),
                        OS2.MPFROMLONG (OS2.BKA_SINGLE));
        OS2.WinSendMsg (Handle[category], OS2.WM_CLOSE, NIL, NIL);
    END Close;

(**************************************************************************)

VAR c: HostCategory;

BEGIN
    UseTNI := FALSE;
    Multidomain := FALSE;
    FOR c := MIN(HostCategory) TO MAX(HostCategory) DO
        PageActive[c] := FALSE;
        OurFontGroup[c] := CommonSettings.NilFontGroup;
        Handle[c] := OS2.NULLHANDLE;
        ChangeInProgress[c] := FALSE;
    END (*FOR*);
END HostLists.

