/****************************************************************/
/*                                                              */
/* Utility to create FtpServer accounts for all users in the    */
/* tcp/ip configuration notebook who have ftp permissions.      */
/* That is, in effect we are migrating ftp accounts from IBM's  */
/* FTPD to FtpServer.  The original data are left intact, in    */
/* case you want to switch back to the original FTPD.           */
/*                                                              */
/* Note: since passwords are encrypted, this script will not    */
/* succeed in migrating the passwords.  I'm afraid you will     */
/* have to enter the passwords manually.                        */
/*                                                              */
/*       Programmer:      P. Moylan                             */
/*       Last modified:   8 June 2004                           */
/*                                                              */
/*   Usage:    migrate                                          */
/*                      (i.e. there are no parameters)          */
/*                                                              */
/****************************************************************/

call RxFuncAdd 'SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs'
call SysLoadFuncs

/* Open the file that was created by the tcp/ip configuration notebook.  */

srcfile = value('ETC',,'OS2ENVIRONMENT')'\tcpnbk.lst'
SAY ''
SAY 'Adding FtpServer users from the database in 'srcfile
SAY ''

CALL Stream srcfile, 'C', 'OPEN READ'
DO FOREVER
    line = LineIn(srcfile)
    IF line = '' THEN LEAVE
    j = POS('(', line)
    IF j > 0 THEN DO
        PARSE VAR line rubbish'('line
        CALL ProcessUser srcfile STRIP(line)
    END
END
CALL Stream srcfile, 'C', 'CLOSE'

EXIT 0

/****************************************************************/

ProcessUser: PROCEDURE

    /* This is the procedure that processes all data for one    */
    /* user.  On entry we have passed the opening '('.  The     */
    /* first parameter is the source file name, and the second  */
    /* is that part of the input line that comes after the '('. */

    PARSE ARG srcfile line
    user.active = 0
    DO FOREVER
        IF LINES(srcfile) = 0 THEN LEAVE
        line = line||STRIP(LineIn(srcfile))
        IF line = ')' THEN LEAVE
        ELSE IF line \= '' THEN DO

            /* Pick up a keyword = value line. */

            PARSE VAR line kwd'='val
            kwd = TRANSLATE(kwd)
            val = STRIP(val)

            IF kwd = 'USERID' THEN user.ID = val
            ELSE IF kwd = 'COMMENT' THEN user.comment = val
            ELSE IF kwd = 'FTPD' THEN DO

                PARSE VAR val rubbish'('line

                /* We are now specifically in the FTPD          */
                /* section of the user data.  Pick up the       */
                /* relevant information.                        */

                line = STRIP(line)
                DO FOREVER
                    IF LINES(srcfile) = 0 THEN LEAVE
                    line = line||STRIP(LineIn(srcfile))
                    IF line = ')' THEN LEAVE
                    ELSE IF line \= '' THEN DO

                        /* Pick up a keyword = value line. */

                        PARSE VAR line kwd'='val
                        kwd = TRANSLATE(kwd)
                        val = STRIP(val)

                        IF kwd = 'ACTIVE' THEN user.active = val
                        ELSE IF kwd = 'READ' THEN user.readdirs = val
                        ELSE IF kwd = 'CANREAD' THEN user.canread = val
                        ELSE IF kwd = 'WRITE' THEN user.writedirs = val
                        ELSE IF kwd = 'CANWRITE' THEN user.canwrite = val

                    END /*IF*/
                    line = ''
                END /*DO FOREVER*/
            END /*ELSE IF*/

            ELSE IF LEFT(val,1) = '(' THEN CALL Skip srcfile val

        END /*IF*/
        line = ''

    END /*DO FOREVER*/

    /* We have now picked up all available data for this user.  */
    /* Check whether this is an active user.  If so, create an  */
    /* INI file entry.                                          */

    inifile = INIFileFor('.', user.ID)
    IF user.ID \= '' THEN DO

        IF user.active = 0 THEN
            SAY 'Username: 'user.ID' inactive, skipped'
        ELSE DO

            user.id = TRANSLATE(user.id, 'abcdefghijklmnopqrstuvwxyz',,
                                         'ABCDEFGHIJKLMNOPQRSTUVWXYZ')

            permstring = SortDirs( user.canread, user.readdirs, user.canwrite, user.writedirs)
            IF user.ID = 'anonymous' THEN
                CALL SysIni inifile, user.ID, 'Category', '02'X
            ELSE
                CALL SysIni inifile, user.ID, 'Category', '03'X
            CALL SysIni inifile, user.ID, 'Notes', user.comment
            CALL SysIni inifile, user.ID, 'Volume', permstring
            SAY 'Username: 'user.ID' added'

        END /*ELSE DO*/

    END /*IF*/

    RETURN

/****************************************************************/

SortDirs: PROCEDURE EXPOSE tree.

    /* This is the tricky part.  We take the read and write     */
    /* directory information, and turn it into a permission     */
    /* tree.                                                    */

    PARSE UPPER ARG canread, readdirs, canwrite, writedirs
    Drives = SysDriveMap('C', 'USED')

    /* Start with an empty tree.  */

    tree.0 = 0

    /* If canread = 0, insert all drives and then record        */
    /* absence of read permission for the readdirs directories. */
    /* If canread = 1, it's a little simpler.                   */

    IF canread = 0 THEN DO
        CALL InsertList '+*', Drives
        CALL InsertList '-*', readdirs
    END /*IF*/
    ELSE
        CALL InsertList '+*', readdirs

    /* Now the same operation for the write permissions. */

    IF canwrite = 0 THEN DO
        CALL InsertList '*+', Drives
        CALL InsertList '*-', writedirs
    END /*IF*/
    ELSE
        CALL InsertList '*+', writedirs

    topcount = TranslateCodes()

    /* The tree is now complete.  Turn it into a permission     */
    /* string.  There are 'topcount' top-level nodes.           */

    IF topcount = 0 THEN permstring = ''
    ELSE IF topcount = 1 THEN permstring = MakePermString(1)
    ELSE DO
        permstring = '"/"=""('MakePermString(1)')'
    END /*ELSE*/

    RETURN permstring

/****************************************************************/

InsertList: PROCEDURE EXPOSE tree.

    /* Inserts a list of directories into the tree. */

    PARSE ARG code, dirlist
    DO WHILE dirlist \= ''
        PARSE VAR dirlist dir dirlist
        CALL Insert code, dir
    END /*DO*/
    RETURN

/****************************************************************/

Insert: PROCEDURE EXPOSE tree.

    /* Inserts a top-level directory into the tree. */

    PARSE ARG code, dir
    dir = TRANSLATE(dir, '\', '/')
    IF RIGHT(dir, 1) = '\' THEN dir = LEFT(dir, LENGTH(dir)-1)
    j = 1
    DO WHILE dir \= ''

        M = LENGTH(tree.j.gname)
        N = LENGTH(dir)

        IF j > tree.0 THEN DO
            tree.0 = j
            tree.j.gname = dir
            tree.j.kode = code
            tree.j.level = 1
            dir = ''
        END

        ELSE IF M <= N THEN DO
            IF POS(tree.j.gname, dir) = 1 THEN DO

                /* Current entry is a leading substring of new entry. */

                IF M = N THEN DO
                    CALL UpdateCode j, code
                END
                ELSE IF SUBSTR(dir,M+1,1) = '\' THEN DO
                    CALL InsertBelow j, code, RIGHT(dir, N-M-1)
                END
                dir = ''
            END /* ELSE IF POS ... */
        END /* IF M <= N */

        ELSE IF POS(dir, tree.j.gname) = 1 THEN DO

            /* New entry is a leading substring of current  */
            /* entry.  Swap them.                           */

            IF SUBSTR(tree.j.gname,N+1,1) = '\' THEN DO
                oldname = RIGHT(tree.j.gname, M-N-1)
                oldcode = tree.j.kode
                tree.j.gname = dir
                tree.j.kode = code
                CALL InsertBelow j, oldcode, oldname
                dir = ''
            END

        END /* ELSE IF POS ... */

        j = j+1

    END /*DO*/

    RETURN

/****************************************************************/

InsertBelow: PROCEDURE EXPOSE tree.

    PARSE ARG pos, code, dir

    /* Inserts a subtree under the tree node at tree.pos. */

    ourlevel = tree.pos.level + 1

    /* Separate out the name as a first component subdir (to    */
    /* be inserted now), and a remainder to be inserted via a   */
    /* recursive call.                                          */

    PARSE VAR dir subdir'\'dir

    DO FOREVER
        pos = pos + 1

        IF (pos > tree.0) | (tree.pos.level < ourlevel) THEN DO

            /* We have found the correct insertion point.  Create a     */
            /* hole in the tree array, and put the new information into */
            /* that hole.                                               */

            CALL InsertHoleHere pos
            tree.pos.gname = subdir
            tree.pos.level = ourlevel
            IF dir = '' THEN tree.pos.kode = code
            ELSE DO
                tree.pos.kode = '**'
                CALL InsertBelow pos, code, dir
            END /* ELSE */
            LEAVE

        END

        ELSE IF tree.pos.level = ourlevel THEN DO
            IF tree.pos.gname = subdir THEN DO

                /* We have a name match. */

                CALL UpdateCode j, code
                IF dir \= '' THEN CALL InsertBelow pos, code, dir
                LEAVE

            END /*IF*/
        END

    END /*DO*/

    RETURN

/****************************************************************/

InsertHoleHere: PROCEDURE EXPOSE tree.

    PARSE ARG pos

    /* Inserts an empty entry at tree.pos.       */

    j = tree.0 + 1
    tree.0 = j
    DO WHILE j > pos
        k = j-1
        tree.j.gname = tree.k.gname
        tree.j.level = tree.k.level
        tree.j.kode = tree.k.kode
        j = k
    END

    RETURN

/****************************************************************/

UpdateCode: PROCEDURE EXPOSE tree.

    PARSE ARG pos, NewCode

    /* Merges NewCode into tree.pos.kode.  The new code   */
    /* replaces the old except when the new code is '*'.  */

    oldread = LEFT(tree.pos.kode, 1)
    oldwrite = RIGHT(tree.pos.kode, 1)
    newread = LEFT(NewCode, 1)
    newwrite = LEFT(NewCode, 1)
    IF newread = '*' THEN newread = oldread
    IF newwrite = '*' THEN newwrite = oldread
    tree.pos.kode = newread||newwrite
    RETURN

/****************************************************************/

TranslateCodes: PROCEDURE EXPOSE tree.

    /* Up until now, for ease of merging new and old codes, the */
    /* tree.j.kode have been two-character codes where the      */
    /* first character gives read permission and the second     */
    /* gives write permission.  The codes during that phase     */
    /* were '+' for permitted, '-' for denied, and '*' for      */
    /* inherit.  The present function converts these to the     */
    /* R+ etc codes that are used in the INI file.  It also     */
    /* removes top-level entries with '--' or '**' codes, since */
    /* at the top level these imply no access at all.           */
    /* Return value: the number of top-level nodes remaining.   */

    topcount = 0

    /* To simplify some special case checking, add a sentinel   */
    /* record at the end of the tree array.                     */

    j = tree.0 +  1
    tree.j.level = 0
    tree.0 = j
    j = 1

    DO FOREVER

        /* We go once around the outermost loop for each tree,  */
        /* i.e. for each level 1 record and its descendants.    */

        IF tree.j.level = 0 THEN LEAVE
        IF (tree.j.kode = '--') | (tree.j.kode = '**') THEN DO

            /* Special case: a no-access top-level node.        */
            /* Delete it and promote its subordinate nodes.     */

            prefix = tree.j.gname||'\'
            j0 = j
            CALL KillNode j
            DO WHILE tree.j.level = 2
                tree.j.level = 1
                tree.j.gname = prefix||tree.j.gname
                j = j + 1
                DO WHILE tree.j.level > 2
                    tree.j.level = tree.j.level - 1
                    j = j + 1
                END /* while level > 2 */
            END /* while level = 2 */
            j = j0

        END /* special case */

        ELSE DO

            /* Normal case, only the .kode field needs changing. */

            readcode = LEFT(tree.j.kode, 1)
            IF readcode = '*' THEN readcode = '-'
            writecode = RIGHT(tree.j.kode, 1)
            IF writecode = '*' THEN writecode = '-'
            tree.j.kode = 'R'||readcode||'W'||writecode
            j = j + 1
            DO WHILE tree.j.level > 1
                readcode = LEFT(tree.j.kode, 1)
                writecode = RIGHT(tree.j.kode, 1)
                newcode = ''
                IF readcode \= '*' THEN newcode = 'R'||readcode
                IF writecode \= '*' THEN newcode = newcode||'W'||writecode
                tree.j.kode = newcode
                j = j + 1
            END /*DO WHILE*/
            topcount = topcount + 1

        END /* handling one tree */

    END /* outermost loop */

    RETURN topcount

/****************************************************************/

MakePermString: PROCEDURE EXPOSE tree.

    /* Walks the tree, and turns it into a permission string. */

    PARSE ARG pos

    ans = ''
    L0 = tree.pos.level
    max = tree.0
    DO WHILE tree.pos.level = L0
        name = '"'||tree.pos.gname||'"'
        ans = ans||name||'='||name||tree.pos.kode
        pos = pos + 1
        IF tree.pos.level > L0 THEN DO
            ans = ans||'('||MakePermString(pos)||')'
            pos = tree.max.gname
        END
        IF tree.pos.level = L0 THEN ans = ans||','
    END /*IF*/

    tree.max.gname = pos

    RETURN ans

/****************************************************************/

KillNode: PROCEDURE EXPOSE tree.

    PARSE ARG pos

    /* Deletes the record at tree.pos.       */

    j = tree.0 - 1
    tree.0 = j
    DO WHILE pos <= j
        k = pos+1
        tree.pos.gname = tree.k.gname
        tree.pos.level = tree.k.level
        tree.pos.kode = tree.k.kode
        pos = k
    END

    RETURN

/****************************************************************/

DumpTree: PROCEDURE EXPOSE tree.

    /* For debugging: writes out the tree. */

    SAY 'Dumping tree with 'tree.0' nodes.'
    DO j= 1 TO tree.0
        lvl = tree.j.level
        SAY COPIES(' ', lvl+1)||lvl'  'tree.j.kode'  'tree.j.gname
    END /*DO*/
    RETURN

/****************************************************************/

Skip: PROCEDURE

    /* On entry, the 'line' argument starts with a '('.  We     */
    /* skip the input until we have passed the matching ')'.    */

    PARSE ARG srcfile line
    nesting = 0
    DO UNTIL nesting = 0
        j = POS('(', line)
        k = POS(')', line)
        IF j = 0 THEN DO
            IF k = 0 THEN DO
                /* No parentheses, get a new line */
                IF LINES(srcfile) = 0 THEN nesting = 0
                ELSE line = STRIP(LineIn(srcfile))
            END /* Case k=0 */
            ELSE DO
                PARSE VAR line rubbish')'line
                nesting = nesting - 1
            END /* Case k>0 */
        END /* Case j=0 */
        ELSE IF (k = 0) | (k > j) THEN DO
            PARSE VAR line rubbish'('line
            nesting = nesting + 1
        END /* Case where left paren comes first */
        ELSE DO
            PARSE VAR line rubbish')'line
            nesting = nesting - 1
        END /* Case where right paren comes first */
    END /*DO*/
    RETURN

/****************************************************************/
/*  Procedure to find the INI file that contains the data for   */
/*  one user.  In most installations this will turn out to be   */
/*  the file called FTPD.INI, but if the option to use multiple */
/*  INI files is activated then the file name is based on a     */
/*  hash coding of the username.                                */
/****************************************************************/

IniFileFor:  PROCEDURE

    PARSE UPPER ARG SrcDir, username
    file = SrcDir'\FTPD.INI'
    HashMax = SysIni(file, '$SYS', 'HashMax')
    IF HashMax = 'ERROR:' THEN HashMax = 0
    ELSE HashMax = C2D(REVERSE(HashMax))
    IF HashMax \= 0 THEN DO
        code = 0
        DO WHILE username <> ''
            ch = LEFT(username,1)
            username = RIGHT(username, LENGTH(username)-1)
            code = (16*code + C2D(ch)) // HashMax
        END
        code = TRANSLATE(FORMAT(code,4), '0', ' ')
        file = SrcDir'\FTPD'code'.INI'
    END
    RETURN file

/****************************************************************/

