Temporary change to fix ConfReg-Issue

Temporary change to fix ConfReg-Issue

:Namespace Files
⍝ Description::
⍝ The Files library provides cross-platform tools for use with native files.
⍝ It is the successor to the Files workspace that was distributed with Dyalog APL up to version 15.0
⍝
⍝ Overview::
⍝ This namespace is completely stand-alone and requires no other code to run
⍝
⍝ Functions::
⍝
⍝ data Append filename              - appends data to existing file
⍝ text AppendText filename          - appends (single-byte) text to filename
⍝ text AppendTextU filename         - appends unicode text to filename (NB: no validation or transformation of encodings, use responsibly!)
⍝ t←Nopen filename                  - create/open native file
⍝ data Put filename                 - Put data into filename
⍝ text PutText filename             - write (single-byte) text to filename
⍝ text PutTextU filename            - writes unicode text to filename, encoding might be specified as 2nd right arg (UTF-8 is dflt)
⍝ (tn name)←{dcf}CreateTemp pattern - create a temporary-file in specified dir and return tie-number
⍝                                     (if dcf=1, create a dcf-file, otherwise native)
⍝ R←GetBom Filename                 - returns the "byte order mark" of a file
⍝ R←{vtv}ReadText filename       - returns content of file (vtv=1: as a vtv of lines, otherwise simple vector)
⍝
⍝ {protect}Copy (from to)           - copies a file, optionally failing if "protect" is set and "to" exists
⍝ R←Exists path                     - check if file or directory exists
⍝ {z}←Delete path                   - delete file or directory
⍝ Move (from to)                    - move a file from "from" to "to"
⍝
⍝ MkDir path                        - make a directory
⍝ RmDir path                        - remove directory
⍝ R←{types}Dir pattern              - a ⎕NINFO-compatible replacement of the "Dir"-fn which was moved to WinFiles (types matches ⍺ of ⎕NINFO)
⍝ R←{pattern} List path             - a cover for "Dir", compatible with MiServer's Files.List
⍝
⍝ Functions to deal with component files:
⍝ c←idx FREAD filename              - read specified component from file (
⍝ t←RDCIFromTS ts                   - convert ⎕TS style data into ⎕FRDCI timestamp form
⍝ ts←TSFromRDCI ts                  - Convert ⎕FRDCI timestamps to ⎕TS form
⍝ ts←TSOfComp tn                    - Find timestamp of component tie,n
⍝ r←filename Fopen tieno            - opens component file (creating it if necessary) and returns tieno. Optional tieno[2] controls exclusive access.
⍝
⍝ Migrating from MiServer/Utils/Files.dyalog::
⍝ MiServer also included an early version of Files.dyalog, but the focus on this re-implementation was compatibility with Files.dws,
⍝ so a few fns were changed and some taken away. Here's a list of differences to consider when migrating to this new class:
⍝
⍝    * Changed:
⍝        * DirExists w -> 1 Exists w
⍝        * GetText w -> 0 ReadText w
⍝        * GetVTV w -> 1 ReadText w
⍝        * {pattern}List path
⍝            * previously would return '.' and '..' - no longer does!
⍝            * List '/dir' will (as it did before) return info about the DirName, but the casing of the name will be the one from the argument, not "normalized", as used to be (maybe even ⎕NINFO should do this?)
⍝
⍝    * Removed
⍝        * GetCurrentDirectory & SetCurrentDirectory
⍝        * LikelyURL
⍝        * SplitFilename - use ⎕NPARTS instead
⍝        * _Filetime_to_TS -->use  WinFiles._Filetime_to_TS
⍝        * _CMD, _SH
⍝        * isWin
⍝        * unixfix
⍝        * APLVersion

    (⎕IO ⎕ML)←1

    Normalize←∊1∘⎕NPARTS ⍝ normalize a file name
    SplitFilename←1∘⎕NPARTS ⍝ splits a file name
    Filename←{∊1↓1⎕NPARTS ⍵} ⍝ return file name only
    eis←{(,∘⊂)⍣((326∊⎕DR ⍵)<2>|≡⍵),⍵} ⍝ Enclose if simple

    ∇ r←data Append name
    ⍝ Append "data" to file specified by "name", creating the file if needed
    ⍝ r - number of bytes written
      r←data{(⎕NUNTIE ⍵)⊢⍺ ⎕NAPPEND ⍵}Nopen name
    ∇

    ∇ r←text AppendText name;tn
    ⍝ Append single-byte "text" to file specified by "name", creating the file if needed
    ⍝ r - number of bytes written
      r←text{(⎕NUNTIE ⍵)⊢⍺ ⎕NAPPEND ⍵(⎕DR' ')}Nopen name
    ∇

    ∇ {r}←text PutText name_encoding;name;encoding;nl
    ⍝ Write text to file (single byte)
    ⍝ name_encoding - filename [encoding]
    ⍝ text - character scalar, vector, matrix or vtv
    ⍝ r - number of bytes written
      :If 1<≡name_encoding
          (name encoding)←name_encoding
      :Else
          name←name_encoding
          encoding←(1+127∨.<⎕UCS text)⊃'ANSI' 'ASCII'
      :EndIf
      :If 1<⍴⍴text
          text←,text,⎕UCS nl←10
      :Else
          nl←(<\(⎕UCS 10 13)∊text)/10 13
      :EndIf
      r←(text encoding nl)⎕NPUT name
    ∇

    ∇ {r}←text PutTextU name_encoding;name;encoding;nl
    ⍝ Write text to file (unicode)
    ⍝ name_encoding - filename [encoding]
    ⍝ text - character scalar, vector, matrix or vtv
    ⍝ r - number of bytes written
      :If 1<≡name_encoding
          (name encoding)←name_encoding
      :Else
          name←name_encoding
          encoding←'UTF-8'
      :EndIf
      :If 1<⍴⍴text
          text←,text,⎕UCS nl←10
      :Else
          nl←(<\(⎕UCS 10 13)∊text)/10 13
      :EndIf
      r←(text encoding nl)⎕NPUT name
    ∇

    ∇ r←{vtv}ReadText name
    ⍝ Return the contents of a text file (single byte or Unicode)
    ⍝ name - the name of the file
    ⍝ vtv - (optional, default = 0) if 1, return contents as a vector of text vectors
    ⍝ r - the contents of the file as specified by vtv
      vtv←{0::⍵ ⋄ vtv}0
      ⎕←'Reading ',name', @ ',,' ',⍕⎕xsi,⎕lc ⍝ temporary to fix ConfReg-Issue
      r←⊃⎕NGET name vtv
    ∇

    ∇ {r}←text AppendTextU name;tn;bom;bytes;num;le;int
    ⍝ Append text to existing file. This will transform the data into the current encoding of the file
    ⍝ but will not modify the file's encoding, i.e. you cannot append unicode to an ASCII-File. (Will throw error)
    ⍝ r=number of bytes written
      int←{¯128+256|128+⍵}
      bom←GetBom name ⋄ bytes←2+2×∨/'32'⍷bom ⋄ le←∨/'LE'⍷bom
      tn←name ⎕NTIE 0
      :Select bom
      :CaseList 'UTF-8' 'UTF-8-NOBOM' ⋄ r←(int'UTF-8'⎕UCS text)⎕NAPPEND tn 83
      :CaseList 'UTF-16BE' 'UTF-16LE' 'UTF-32BE' 'UTF-32LE' 'UTF-16' 'UTF-32'
          num←(bytes⍴256)⊤(6↑bom)⎕UCS text
          num←int,⍉le{⍺:⊖⍵ ⋄ ⍵}num
          {}num ⎕NAPPEND tn 83
      :Else
          ('Unrecognized bom: ',bom)⎕SIGNAL 11 ⍝ issue DOMAIN ERROR if file's bom not recogized
      :EndSelect
      ⎕NUNTIE tn
    ∇

    ∇ r←filename Fopen tn
    ⍝ Ties a component file (creates if not found) and returns tieno.
    ⍝ tn[1] is the tie number (0 to get next available)
    ⍝ tn[2] is optional (default=0) where a value of 1 indicates to tie exclusively
      tn←2↑tn ⍝ default to shared tie
      :Trap 22
          r←filename{⍵[2]:⍺ ⎕FTIE ⍵[1] ⋄ ⍺ ⎕FSTIE ⍵[1]}tn
      :Else
          r←filename{⍵[2]:⍵[3] ⋄ ⍺ ⎕FSTIE ⍵[1]⊣⎕FUNTIE ⍵[3]}tn,filename ⎕FCREATE tn[1]
      :EndTrap
    ∇

    ∇ r←comp FREAD filename
    ⍝ Read component comp from file named filename
    ⍝ FREAD ties, reads the component, and unties
      r←comp{⍺{(⎕FUNTIE ⍵)⊢⎕FREAD ⍵ ⍺}⍵ ⎕FSTIE 0}filename
    ∇

    ∇ r←filename Nopen tn
    ⍝ Opens (ties) a native file (creates if not found) and returns tieno.
    ⍝ tn[1] is the tie number (0 to get next available)
    ⍝ tn[2] is optional (default=0) where a value of 1 indicates to create the file only and fail (returning 0) if it already exists
      tn←2↑tn
      :Trap 0
          r←filename ⎕NCREATE tn[1]
      :Else
          →tn[2]⍴r←0
          r←filename ⎕NTIE tn[1]
      :EndTrap
    ∇

    ∇ {r}←data Put args;name;tn;dontOverwrite
    ⍝ Puts data to a native file, either creating the file or overwriting it if it exists
    ⍝ data - the data to be written
    ⍝ args[1] - the name of the file to write to
    ⍝ args[2] - (optional) is 1 to indicate NOT to overwrite an existing file,
    ⍝           otherwise 0 (or no second argument will cause an existing file to be overwritten
    ⍝ r - number of bytes written or ¯1 if the file exists and was not to be overwritten
      args←eis args
      (name dontOverwrite)←args,(⍴args)↓'' 0
      :If 0≠tn←dontOverwrite Nopen name
          r←data{(⎕NUNTIE ⍵)⊢⍺ ⎕NAPPEND(0 ⎕NRESIZE ⍵)(⎕DR ⍺)}tn
      :Else
          r←¯1
      :EndIf
    ∇

    ∇ r←GetBom Filename;nu;bin2hex
    ⍝ returns the BOM of specified file (matching the file-encodings documented @ http://help.dyalog.com/16.0/Content/Language/System%20Functions/nget.htm#Encodings )
      bin2hex←{(⎕D,⎕A)[1+2⊥⍉((0.25×⍴⍵),4)⍴⍵]}  ⍝ may have a small impact on performance, but improves readability
      nu←Filename ⎕NTIE 0 64  ⍝ asking for the BOM of a non-existent file will cause an error
    ⍝ look at the first bytes and try to determine encoding
      :Select 1
      :Case 'EFBBBF'≡bin2hex ⎕NREAD nu,11 3 0 ⋄ r←'UTF-8'
      :Case '0000FEFF'≡bin2hex ⎕NREAD nu,11 4 0 ⋄ r←'UTF-32BE' ⍝ Big Endian
      :Case 'FFFE0000'≡bin2hex ⎕NREAD nu,11 4 0 ⋄ r←'UTF-32LE' ⍝ Little Endian
      :Case 'FEFF'≡bin2hex ⎕NREAD nu,11 2 0 ⋄ r←'UTF-16BE'     ⍝ Big Endian
      :Case 'FFFE'≡bin2hex ⎕NREAD nu,11 2 0 ⋄ r←'UTF-16LE'     ⍝ Little Endian
      :Else  ⍝ if the file had no BOM
          ⎕NUNTIE nu
          r←2⊃⎕NGET Filename   ⍝ read it with ⎕NGET and get the encoding from its result...
      :EndSelect
      ⎕NUNTIE nu
    ∇

    ∇ {protect}Copy FmTo;nu1;nu2;seg;fSize1;pos
    ⍝ Copies a file
    ⍝ FmTo - 2 element vector of [1] the source file name, [2] the destination file name
    ⍝ protect - optional flag indicating whether to fail if destination file (protect=1) exists, default is 0
      protect←{6::⍵ ⋄ protect}0
      nu1←nu2←0
      ⎕SIGNAL(protect∧Exists 2⊃FmTo)/⊂('EN' 22)('Message' 'Destination file exists')
      :Trap 0
          Delete 2⊃FmTo
          nu1←(1⊃FmTo)⎕NTIE 0
          nu2←(2⊃FmTo)⎕NCREATE 0
          seg←(fSize1←⎕NSIZE nu1)⌊0.2×⎕WA  ⍝ length of segments per pass: use 20% of the free ws
          pos←0  ⍝ start at position 0
          :Repeat
              (⎕NREAD nu1,83,seg,pos)⎕NAPPEND nu2,83
              pos+←seg
          :Until pos≥fSize1
          ⎕NUNTIE nu1,nu2
      :Else
          ⎕NUNTIE nu1,nu2
          ⎕SIGNAL⊂'EN' 'EM' 'Message'{⍺(⍵⍎⍺)}¨⎕DMX
      :EndTrap
    ∇

    ∇ {r}←{x}Delete name
    ⍝ Delete specified file or directory
    ⍝ name - the file or directory name
    ⍝ x - optional Boolean to indicate to NOT signal an error if the file does not exist
    ⍝ r - 1 if a file/directory was deleted, 0 otherwise (file/directory didn't exist)
      x←{6::⍵ ⋄ x}0
      :Trap 0
          r←x ⎕NDELETE name
      :Else
          ⎕SIGNAL⊂'EN' 'EM' 'Message'{⍺(⍵⍎⍺)}¨⎕DMX
      :EndTrap
    ∇

    ∇ r←{types}Dir path;dir
    ⍝ Cover for ⎕NINFO wildcarded file search
    ⍝ path - character vector file path to search (may include wildcards)
    ⍝ types - (optional) the types of information to return, if omitted, just return the matching filenames
    ⍝ r - a vector of vectors, with each element corresponding to an element in types
    ⍝
    ⍝ Note: this function differs in syntax from the Dir utility found in the files workspace in versions of Dyalog old than v16
    ⍝       The old version is available in WinFiles for Windows-only

      :If 0=⎕NC'types'
          types←⊢
      :EndIf
      r←,types(⎕NINFO⍠1)path
    ∇

    ∇ r←{pattern}List path;hasPattern;isFolder
    ⍝ path and pattern are related.
    ⍝ If there is no pattern (or pattern is empty)
    ⍝   If path ends with '/' or '\' then return information for the contents of the folder, otherwise, return information about the folder (or file) itself
    ⍝ If a non-empty pattern exists, it is used as a filter on the contents of path (path is treated as a folder name)
    ⍝ Examples:
    ⍝   List '/dir/foo'  ⍝ returns information about /dir/foo
    ⍝   List '/dir/foo/' ⍝ returns information about the contents of /dir/foo/
    ⍝   '*.dyalog' List '/dir/foo'  ⍝ returns information about the .dyalog files in /dir/foo/
    ⍝   List '/dir/foo/*.dyalog'    ⍝ also returns information about the .dyalog files in /dir/foo/
    ⍝ Information returned is:
    ⍝ [;0] Name [;1] Length [;2] LastAccessTime [;3] IsDirectory
    ⍝ (cover for MiServer's Files.List)

      path←Normalize path
      hasPattern←~0∊⍴pattern←{6::⍵ ⋄ pattern}''
      isFolder←'/'=¯1↑path
      :If isFolder≠hasPattern
          pattern←'/',pattern,hasPattern↓'*'
      :EndIf

      r←⍉↑0 2 3 1(⎕NINFO⍠1)path,pattern
      r[;4]←r[;4]=1           ⍝ flag directories
      r[;1]←Filename¨r[;1]  ⍝ remove paths
    ∇

      Exists←{
      ⍝ test for existence of file or folder
      ⍝ ⍺ - optional flag where 1 indicates test for folder (default is 0)
          ⍺←0 ⋄ ⎕NEXISTS ⍵,⍺/'/'}

    DirExists←1∘Exists

      MkDir←{
      ⍝ create a directory (cover for ⎕MKDIR)
          ⍺←⊢ ⋄ ⍺ ⎕MKDIR ⍵}

    ∇ Move filenames
    ⍝ move/rename file
    ⍝ filenames - [1] source file name [2] destination file name
      :Trap 0
          ⎕NUNTIE(2⊃filenames)⎕NRENAME((1⊃filenames)⎕NTIE 0)
      :Else
          ⎕SIGNAL⊂'EN' 'EM' 'Message'{⍺(⍵⍎⍺)}¨⎕DMX
      :EndTrap
    ∇

    ∇ RmDir path
    ⍝ removes directory specified in path.
    ⍝ path - directory name to remove
      :If 1 Exists path
          :Trap 0
              ⎕NDELETE path
          :Else
              ⎕SIGNAL⊂'EN' 'EM' 'Message'{⍺(⍵⍎⍺)}¨⎕DMX
          :EndTrap
      :Else
          ⎕SIGNAL⊂('EN' 22)('Message' 'Directory does not exist')
      :EndIf
    ∇

    ∇ t←RDCIFromTS ts;s;⎕IO
    ⍝ Accepts a numeric vector or array in ⎕TS format with 7≥¯1↑⍴argument
    ⍝ Returns time and date in ⎕FRDCI format with shape ¯1↓⍴argument

      ⎕IO←0 ⋄ s←¯1↓⍴ts ⋄ ts←((1⌈×/s),¯1↑⍴ts)⍴ts
      ts←((1↑⍴ts),7)↑ts ⍝ years<100 are in window 1950-2049
      ts[;0]+←(ts[;0]≤99)×(50/2000 1900)[100|ts[;0]]
      ts[;6]×←6÷100     ⍝ turn milisecs into 1/60th units
      t←¯60 ¯29 ¯1 30 60 91 121 152 183 213 244 274[ts[;1]-1]
      t-←(4|ts[;0])<2≥ts[;1]
      t←(t+⌊365.25×¯60+1900|ts[;0]),0 2↓ts
      t←¯18626112000+s⍴1 1 24 60 60 60⊥⍉t
    ∇

    ∇ ts←TSFromRDCI ts;md;s;sm;yr;z;⎕IO
    ⍝ Returns ⎕TS style timestamps (shape (⍴argument),7) - note that RDCI is UTC, so some Timezone-Conversion might be required...

      ⎕IO←0 ⋄ s←⍴ts ⋄ ts←,ts+18626112000
      md←365.2501|1+1461|yr←⌊ts÷5184000
      sm←31 61 92 122 153 184 214 245 275 306 337 366
      z←(,⍉<⍀sm∘.≥md)/,((⍴md),12)⍴⍳12
      md←(1+12|z+2),[0.1]⌈md-(0,sm)[z]
      ts←(1960+⌊(yr+60)÷365.25),md,⍉24 60 60 60⊤ts
      ts[;6]←⌊0.5+ts[;6]×100÷6
      ts←(s,7)⍴ts
    ∇

    ∇ ts←ComponentTS tncomp
    ⍝ Returns ⎕TS-style component timestamp
    ⍝ tncomp - [1] tie number, [2] component number
      ts←TSFromRDCI(2+⎕IO)⊃⎕FRDCI tncomp
    ∇

    ∇ (tn name)←{dcf}CreateTemp pattern;folder;i;cnt;z
    ⍝ Create a temporary file based on pattern e.g. c:\folder\*.ext
    ⍝ pattern - the local
    ⍝ Will create the file as .DCF if dcf=1, native file otherwise.
      dcf←{6::⍵ ⋄ dcf}0
      pattern←Normalize pattern
      ⎕SIGNAL(1≠'*'+.=pattern)/⊂('EN' 11)('Message' 'pattern must contain exactly 1 *')
      folder←⊃SplitFilename pattern
      i←pattern⍳'*'

      tn←cnt←0 ⋄ z←⊂''
      :Trap 102 ⋄ z←folder∘,¨Dir pattern ⋄ :EndTrap

      :Repeat ⍝ Avoid race conditions
          name←1⊃((⊂((i-1)↑pattern),'temp_',(⍕cnt),'_'),¨(⍕¨⍳1+⍴z),¨⊂i↓pattern)~z
          :If dcf
              :Trap 22 ⋄ tn←name ⎕FCREATE 0 ⋄ :EndTrap
          :Else
              :Trap 22 ⋄ tn←name ⎕NCREATE 0 ⋄ :EndTrap
          :EndIf
          cnt←cnt+1
      :Until (cnt>20)∨tn≠0
      ⎕SIGNAL tn↓⊂('EN' 22)('Message' 'Unable to create temporary file')
    ∇

    :Section Documentation Utilities
    ⍝ these are generic utilities used for documentation

    ∇ docn←ExtractDocumentationSections describeOnly;⎕IO;box;CR;sections;showMe;tit;z;docn∆
    ⍝ internal utility function
      ⎕IO←1
      CR←⎕UCS 13
      box←{{⍵{⎕AV[(1,⍵,1)/223 226 222],CR,⎕AV[231],⍺,⎕AV[231],CR,⎕AV[(1,⍵,1)/224 226 221]}⍴⍵}(⍵~CR),' '}
      docn←1↓⎕SRC ⎕THIS
      docn←1↓¨docn/⍨∧\'⍝'=⊃¨docn ⍝ keep all contiguous comments
      docn←docn/⍨'⍝'≠⊃¨docn     ⍝ remove any lines beginning with ⍝⍝
      sections←{∨/'::'⍷⍵}¨docn
      :If 11=⎕DR'describeOnly'
      :AndIf describeOnly
          (sections docn)←((2>+\sections)∘/¨sections docn)
      :ElseIf (⎕DR' ')=⎕DR'describeOnly'   ⍝ alternate usage: pass a string containing name of sections to show  (or a vtv of names)
          describeOnly←(819⌶)describeOnly
          tit←(819⌶)sections/docn
          docn∆←⍬
          :For showMe :In ,{1=≡⍵:⊂⍵ ⋄ ⍵}describeOnly
              :If ∨/z←∨/¨showMe∘⍷¨tit
                  docn∆,←((z⍳1)=+\sections)/docn
              :EndIf
          :EndFor
          sections←{∨/'::'⍷⍵}¨docn←docn∆
      :EndIf
      (sections/docn)←box¨sections/docn
      docn←∊docn,¨CR
    ∇

    ∇ r←Documentation
    ⍝ return full documentation
      :Access public shared
      r←ExtractDocumentationSections 0
    ∇

    ∇ r←Describe
    ⍝ return description only
      :Access public shared
      r←ExtractDocumentationSections 1
    ∇

    ∇ r←ShowDoc look4
    ⍝ only show section that contain argument in their title
      :Access public shared
      r←ExtractDocumentationSections look4
    ∇

    ∇ R←Version
      :Access public shared
      R←'Files' '1.0.0' '2017-08-08'
    ∇

    :EndSection
:EndNamespace

:Namespace Files
⍝ Description::
⍝ The Files library provides cross-platform tools for use with native files.
⍝ It is the successor to the Files workspace that was distributed with Dyalog APL up to version 15.0
⍝
⍝ Overview::
⍝ This namespace is completely stand-alone and requires no other code to run
⍝
⍝ Functions::
⍝
⍝ data Append filename              - appends data to existing file
⍝ text AppendText filename          - appends (single-byte) text to filename
⍝ text AppendTextU filename         - appends unicode text to filename (NB: no validation or transformation of encodings, use responsibly!)
⍝ t←Nopen filename                  - create/open native file
⍝ data Put filename                 - Put data into filename
⍝ text PutText filename             - write (single-byte) text to filename
⍝ text PutTextU filename            - writes unicode text to filename, encoding might be specified as 2nd right arg (UTF-8 is dflt)
⍝ (tn name)←{dcf}CreateTemp pattern - create a temporary-file in specified dir and return tie-number
⍝                                     (if dcf=1, create a dcf-file, otherwise native)
⍝ R←GetBom Filename                 - returns the "byte order mark" of a file
⍝ R←{nul}ReadAllLines filename      - returns vtv with content of file
⍝ R←{vtv}ReadAllText filename       - returns content of file (vtv=1: as a vtv of lines, otherwise simple vector)
⍝
⍝ {protect}Copy (from to)           - copies a file, optionally failing if "protect" is set and "to" exists
⍝ R←Exists path                     - check if file or directory exists
⍝ {z}←Delete path                   - delete file or directory
⍝ Move (from to)                    - move a file from "from" to "to"
⍝
⍝ MkDir path                        - make a directory
⍝ RmDir path                        - remove directory
⍝ R←{types}Dir pattern              - a ⎕NINFO-compatible replacement of the "Dir"-fn which was moved to WinFiles (types matches ⍺ of ⎕NINFO)
⍝ R←{pattern} List path             - a cover for "Dir", compatible with MiServer's Files.List
⍝
⍝ Functions to deal with component files:
⍝ c←idx FREAD filename              - read specified component from file (
⍝ t←RDCIFromTS ts                   - convert ⎕TS style data into ⎕FRDCI timestamp form
⍝ ts←TSFromRDCI ts                  - Convert ⎕FRDCI timestamps to ⎕TS form
⍝ ts←TSOfComp tn                    - Find timestamp of component tie,n
⍝ r←filename Fopen tieno            - opens component file (creating it if necessary) and returns tieno. Optional tieno[2] controls exclusive access.
⍝
⍝ Migrating from MiServer/Utils/Files.dyalog::
⍝ MiServer also included an early version of Files.dyalog, but the focus on this re-implementation was compatibility with Files.dws,
⍝ so a few fns were changed and some taken away. Here's a list of differences to consider when migrating to this new class:
⍝
⍝    * Changed:
⍝        * DirExists w -> 1 Exists w
⍝        * GetText w -> 0 ReadAllText w
⍝        * GetVTV w -> 1 ReadAllText w
⍝        * {pattern}List path
⍝            * previously would return '.' and '..' - no longer does!
⍝            * List '/dir' will (as it did before) return info about the DirName, but the casing of the name will be the one from the argument, not "normalized", as used to be (maybe even ⎕NINFO should do this?)
⍝
⍝    * Removed
⍝        * GetCurrentDirectory & SetCurrentDirectory
⍝        * LikelyURL
⍝        * SplitFilename - use ⎕NPARTS instead
⍝        * _Filetime_to_TS -->use  WinFiles._Filetime_to_TS
⍝        * _CMD, _SH
⍝        * isWin
⍝        * unixfix
⍝        * APLVersion

    (⎕IO ⎕ML)←1

    Normalize←∊1∘⎕NPARTS ⍝ normalize a file name
    SplitFilename←1∘⎕NPARTS ⍝ splits a file name
    Filename←{∊1↓1⎕NPARTS ⍵} ⍝ return file name only
    eis←{(,∘⊂)⍣((326∊⎕DR ⍵)<2>|≡⍵),⍵} ⍝ Enclose if simple

    ∇ r←data Append name
    ⍝ Append "data" to file specified by "name", creating the file if needed
    ⍝ r - number of bytes written
      r←data{(⎕NUNTIE ⍵)⊢⍺ ⎕NAPPEND ⍵}Nopen name
    ∇

    ∇ r←text AppendText name;tn
    ⍝ Append single-byte "text" to file specified by "name", creating the file if needed
    ⍝ r - number of bytes written
      r←text{(⎕NUNTIE ⍵)⊢⍺ ⎕NAPPEND ⍵(⎕DR' ')}Nopen name
    ∇

    ∇ {r}←text PutText name_encoding;name;encoding;nl
    ⍝ Write text to file (single byte)
    ⍝ name_encoding - filename [encoding]
    ⍝ text - character scalar, vector, matrix or vtv
    ⍝ r - number of bytes written
      :If 1<≡name_encoding
          (name encoding)←name_encoding
      :Else
          name←name_encoding
          encoding←(1+127∨.<⎕UCS text)⊃'ANSI' 'ASCII'
      :EndIf
      :If 1<⍴⍴text
          text←,text,⎕UCS nl←10
      :Else
          nl←(<\(⎕UCS 10 13)∊text)/10 13
      :EndIf
      r←(text encoding nl)⎕NPUT name
    ∇

    ∇ {r}←text PutTextU name_encoding;name;encoding;nl
    ⍝ Write text to file (unicode)
    ⍝ name_encoding - filename [encoding]
    ⍝ text - character scalar, vector, matrix or vtv
    ⍝ r - number of bytes written
      :If 1<≡name_encoding
          (name encoding)←name_encoding
      :Else
          name←name_encoding
          encoding←'UTF-8'
      :EndIf
      :If 1<⍴⍴text
          text←,text,⎕UCS nl←10
      :Else
          nl←(<\(⎕UCS 10 13)∊text)/10 13
      :EndIf
      r←(text encoding nl)⎕NPUT name
    ∇

    ∇ r←{vtv}ReadText name
    ⍝ Return the contents of a text file (single byte or Unicode)
    ⍝ name - the name of the file
    ⍝ vtv - (optional, default = 0) if 1, return contents as a vector of text vectors
    ⍝ r - the contents of the file as specified by vtv
      vtv←{0::⍵ ⋄ vtv}0
      r←⊃⎕NGET name vtv
    ∇

    ∇ {r}←text AppendTextU name;tn;bom;bytes;num;le;int
    ⍝ Append text to existing file. This will transform the data into the current encoding of the file
    ⍝ but will not modify the file's encoding, i.e. you cannot append unicode to an ASCII-File. (Will throw error)
    ⍝ r=number of bytes written
      int←{¯128+256|128+⍵}
      bom←GetBom name ⋄ bytes←2+2×∨/'32'⍷bom ⋄ le←∨/'LE'⍷bom
      tn←name ⎕NTIE 0
      :Select bom
      :CaseList 'UTF-8' 'UTF-8-NOBOM' ⋄ r←(int'UTF-8'⎕UCS text)⎕NAPPEND tn 83
      :CaseList 'UTF-16BE' 'UTF-16LE' 'UTF-32BE' 'UTF-32LE' 'UTF-16' 'UTF-32'
          num←(bytes⍴256)⊤(6↑bom)⎕UCS text
          num←int,⍉le{⍺:⊖⍵ ⋄ ⍵}num
          {}num ⎕NAPPEND tn 83
      :Else
          ('Unrecognized bom: ',bom)⎕SIGNAL 11 ⍝ issue DOMAIN ERROR if file's bom not recogized
      :EndSelect
      ⎕NUNTIE tn
    ∇

    ∇ r←filename Fopen tn
    ⍝ Ties a component file (creates if not found) and returns tieno.
    ⍝ tn[1] is the tie number (0 to get next available)
    ⍝ tn[2] is optional (default=0) where a value of 1 indicates to tie exclusively
      tn←2↑tn ⍝ default to shared tie
      :Trap 22
          r←filename{⍵[2]:⍺ ⎕FTIE ⍵[1] ⋄ ⍺ ⎕FSTIE ⍵[1]}tn
      :Else
          r←filename{⍵[2]:⍵[3] ⋄ ⍺ ⎕FSTIE ⍵[1]⊣⎕FUNTIE ⍵[3]}tn,filename ⎕FCREATE tn[1]
      :EndTrap
    ∇

    ∇ r←comp FREAD filename
    ⍝ Read component comp from file named filename
    ⍝ FREAD ties, reads the component, and unties
      r←comp{⍺{(⎕FUNTIE ⍵)⊢⎕FREAD ⍵ ⍺}⍵ ⎕FSTIE 0}filename
    ∇

    ∇ r←filename Nopen tn
    ⍝ Opens (ties) a native file (creates if not found) and returns tieno.
    ⍝ tn[1] is the tie number (0 to get next available)
    ⍝ tn[2] is optional (default=0) where a value of 1 indicates to create the file only and fail (returning 0) if it already exists
      tn←2↑tn
      :Trap 0
          r←filename ⎕NCREATE tn[1]
      :Else
          →tn[2]⍴r←0
          r←filename ⎕NTIE tn[1]
      :EndTrap
    ∇

    ∇ {r}←data Put args;name;tn;dontOverwrite
    ⍝ Puts data to a native file, either creating the file or overwriting it if it exists
    ⍝ data - the data to be written
    ⍝ args[1] - the name of the file to write to
    ⍝ args[2] - (optional) is 1 to indicate NOT to overwrite an existing file,
    ⍝           otherwise 0 (or no second argument will cause an existing file to be overwritten
    ⍝ r - number of bytes written or ¯1 if the file exists and was not to be overwritten
      args←eis args
      (name dontOverwrite)←args,(⍴args)↓'' 0
      :If 0≠tn←dontOverwrite Nopen name
          r←data{(⎕NUNTIE ⍵)⊢⍺ ⎕NAPPEND(0 ⎕NRESIZE ⍵)(⎕DR ⍺)}tn
      :Else
          r←¯1
      :EndIf
    ∇

    ∇ r←GetBom Filename;nu;bin2hex
    ⍝ returns the BOM of specified file (matching the file-encodings documented @ http://help.dyalog.com/16.0/Content/Language/System%20Functions/nget.htm#Encodings )
      bin2hex←{(⎕D,⎕A)[1+2⊥⍉((0.25×⍴⍵),4)⍴⍵]}  ⍝ may have a small impact on performance, but improves readability
      nu←Filename ⎕NTIE 0 64  ⍝ asking for the BOM of a non-existent file will cause an error
    ⍝ look at the first bytes and try to determine encoding
      :Select 1
      :Case 'EFBBBF'≡bin2hex ⎕NREAD nu,11 3 0 ⋄ r←'UTF-8'
      :Case '0000FEFF'≡bin2hex ⎕NREAD nu,11 4 0 ⋄ r←'UTF-32BE' ⍝ Big Endian
      :Case 'FFFE0000'≡bin2hex ⎕NREAD nu,11 4 0 ⋄ r←'UTF-32LE' ⍝ Little Endian
      :Case 'FEFF'≡bin2hex ⎕NREAD nu,11 2 0 ⋄ r←'UTF-16BE'     ⍝ Big Endian
      :Case 'FFFE'≡bin2hex ⎕NREAD nu,11 2 0 ⋄ r←'UTF-16LE'     ⍝ Little Endian
      :Else  ⍝ if the file had no BOM
          ⎕NUNTIE nu
          r←2⊃⎕NGET Filename   ⍝ read it with ⎕NGET and get the encoding from its result...
      :EndSelect
      ⎕NUNTIE nu
    ∇

    ∇ {protect}Copy FmTo;nu1;nu2;seg;fSize1;pos
    ⍝ Copies a file
    ⍝ FmTo - 2 element vector of [1] the source file name, [2] the destination file name
    ⍝ protect - optional flag indicating whether to fail if destination file (protect=1) exists, default is 0
      protect←{6::⍵ ⋄ protect}0
      nu1←nu2←0
      ⎕SIGNAL(protect∧Exists 2⊃FmTo)/⊂('EN' 22)('Message' 'Destination file exists')
      :Trap 0
          Delete 2⊃FmTo
          nu1←(1⊃FmTo)⎕NTIE 0
          nu2←(2⊃FmTo)⎕NCREATE 0
          seg←(fSize1←⎕NSIZE nu1)⌊0.2×⎕WA  ⍝ length of segments per pass: use 20% of the free ws
          pos←0  ⍝ start at position 0
          :Repeat
              (⎕NREAD nu1,83,seg,pos)⎕NAPPEND nu2,83
              pos+←seg
          :Until pos≥fSize1
          ⎕NUNTIE nu1,nu2
      :Else
          ⎕NUNTIE nu1,nu2
          ⎕SIGNAL⊂'EN' 'EM' 'Message'{⍺(⍵⍎⍺)}¨⎕DMX
      :EndTrap
    ∇

    ∇ {r}←{x}Delete name
    ⍝ Delete specified file or directory
    ⍝ name - the file or directory name
    ⍝ x - optional Boolean to indicate to NOT signal an error if the file does not exist
    ⍝ r - 1 if a file/directory was deleted, 0 otherwise (file/directory didn't exist)
      x←{6::⍵ ⋄ x}0
      :Trap 0
          r←x ⎕NDELETE name
      :Else
          ⎕SIGNAL⊂'EN' 'EM' 'Message'{⍺(⍵⍎⍺)}¨⎕DMX
      :EndTrap
    ∇

    ∇ r←{types}Dir path;dir
    ⍝ Cover for ⎕NINFO wildcarded file search
    ⍝ path - character vector file path to search (may include wildcards)
    ⍝ types - (optional) the types of information to return, if omitted, just return the matching filenames
    ⍝ r - a vector of vectors, with each element corresponding to an element in types
    ⍝
    ⍝ Note: this function differs in syntax from the Dir utility found in the files workspace in versions of Dyalog old than v16
    ⍝       The old version is available in WinFiles for Windows-only

      :If 0=⎕NC'types'
          types←⊢
      :EndIf
      r←,types(⎕NINFO⍠1)path
    ∇

    ∇ r←{pattern}List path;hasPattern;isFolder
    ⍝ path and pattern are related.
    ⍝ If there is no pattern (or pattern is empty)
    ⍝   If path ends with '/' or '\' then return information for the contents of the folder, otherwise, return information about the folder (or file) itself
    ⍝ If a non-empty pattern exists, it is used as a filter on the contents of path (path is treated as a folder name)
    ⍝ Examples:
    ⍝   List '/dir/foo'  ⍝ returns information about /dir/foo
    ⍝   List '/dir/foo/' ⍝ returns information about the contents of /dir/foo/
    ⍝   '*.dyalog' List '/dir/foo'  ⍝ returns information about the .dyalog files in /dir/foo/
    ⍝   List '/dir/foo/*.dyalog'    ⍝ also returns information about the .dyalog files in /dir/foo/
    ⍝ Information returned is:
    ⍝ [;0] Name [;1] Length [;2] LastAccessTime [;3] IsDirectory
    ⍝ (cover for MiServer's Files.List)

      path←Normalize path
      hasPattern←~0∊⍴pattern←{6::⍵ ⋄ pattern}''
      isFolder←'/'=¯1↑path
      :If isFolder≠hasPattern
          pattern←'/',pattern,hasPattern↓'*'
      :EndIf

      r←⍉↑0 2 3 1(⎕NINFO⍠1)path,pattern
      r[;4]←r[;4]=1           ⍝ flag directories
      r[;1]←Filename¨r[;1]  ⍝ remove paths
    ∇

      Exists←{
      ⍝ test for existence of file or folder
      ⍝ ⍺ - optional flag where 1 indicates test for folder (default is 0)
          ⍺←0 ⋄ ⎕NEXISTS ⍵,⍺/'/'}

    DirExists←1∘Exists

      MkDir←{
      ⍝ create a directory (cover for ⎕MKDIR)
          ⍺←⊢ ⋄ ⍺ ⎕MKDIR ⍵}

    ∇ Move filenames
    ⍝ move/rename file
    ⍝ filenames - [1] source file name [2] destination file name
      :Trap 0
          ⎕NUNTIE(2⊃filenames)⎕NRENAME((1⊃filenames)⎕NTIE 0)
      :Else
          ⎕SIGNAL⊂'EN' 'EM' 'Message'{⍺(⍵⍎⍺)}¨⎕DMX
      :EndTrap
    ∇

    ∇ RmDir path
    ⍝ removes directory specified in path.
    ⍝ path - directory name to remove
      :If 1 Exists path
          :Trap 0
              ⎕NDELETE path
          :Else
              ⎕SIGNAL⊂'EN' 'EM' 'Message'{⍺(⍵⍎⍺)}¨⎕DMX
          :EndTrap
      :Else
          ⎕SIGNAL⊂('EN' 22)('Message' 'Directory does not exist')
      :EndIf
    ∇

    ∇ t←RDCIFromTS ts;s;⎕IO
    ⍝ Accepts a numeric vector or array in ⎕TS format with 7≥¯1↑⍴argument
    ⍝ Returns time and date in ⎕FRDCI format with shape ¯1↓⍴argument

      ⎕IO←0 ⋄ s←¯1↓⍴ts ⋄ ts←((1⌈×/s),¯1↑⍴ts)⍴ts
      ts←((1↑⍴ts),7)↑ts ⍝ years<100 are in window 1950-2049
      ts[;0]+←(ts[;0]≤99)×(50/2000 1900)[100|ts[;0]]
      ts[;6]×←6÷100     ⍝ turn milisecs into 1/60th units
      t←¯60 ¯29 ¯1 30 60 91 121 152 183 213 244 274[ts[;1]-1]
      t-←(4|ts[;0])<2≥ts[;1]
      t←(t+⌊365.25×¯60+1900|ts[;0]),0 2↓ts
      t←¯18626112000+s⍴1 1 24 60 60 60⊥⍉t
    ∇

    ∇ ts←TSFromRDCI ts;md;s;sm;yr;z;⎕IO
    ⍝ Returns ⎕TS style timestamps (shape (⍴argument),7) - note that RDCI is UTC, so some Timezone-Conversion might be required...

      ⎕IO←0 ⋄ s←⍴ts ⋄ ts←,ts+18626112000
      md←365.2501|1+1461|yr←⌊ts÷5184000
      sm←31 61 92 122 153 184 214 245 275 306 337 366
      z←(,⍉<⍀sm∘.≥md)/,((⍴md),12)⍴⍳12
      md←(1+12|z+2),[0.1]⌈md-(0,sm)[z]
      ts←(1960+⌊(yr+60)÷365.25),md,⍉24 60 60 60⊤ts
      ts[;6]←⌊0.5+ts[;6]×100÷6
      ts←(s,7)⍴ts
    ∇

    ∇ ts←ComponentTS tncomp
    ⍝ Returns ⎕TS-style component timestamp
    ⍝ tncomp - [1] tie number, [2] component number
      ts←TSFromRDCI(2+⎕IO)⊃⎕FRDCI tncomp
    ∇

    ∇ (tn name)←{dcf}CreateTemp pattern;folder;i;cnt;z
    ⍝ Create a temporary file based on pattern e.g. c:\folder\*.ext
    ⍝ pattern - the local
    ⍝ Will create the file as .DCF if dcf=1, native file otherwise.
      dcf←{6::⍵ ⋄ dcf}0
      pattern←Normalize pattern
      ⎕SIGNAL(1≠'*'+.=pattern)/⊂('EN' 11)('Message' 'pattern must contain exactly 1 *')
      folder←⊃SplitFilename pattern
      i←pattern⍳'*'

      tn←cnt←0 ⋄ z←⊂''
      :Trap 102 ⋄ z←folder∘,¨Dir pattern ⋄ :EndTrap

      :Repeat ⍝ Avoid race conditions
          name←1⊃((⊂((i-1)↑pattern),'temp_',(⍕cnt),'_'),¨(⍕¨⍳1+⍴z),¨⊂i↓pattern)~z
          :If dcf
              :Trap 22 ⋄ tn←name ⎕FCREATE 0 ⋄ :EndTrap
          :Else
              :Trap 22 ⋄ tn←name ⎕NCREATE 0 ⋄ :EndTrap
          :EndIf
          cnt←cnt+1
      :Until (cnt>20)∨tn≠0
      ⎕SIGNAL tn↓⊂('EN' 22)('Message' 'Unable to create temporary file')
    ∇

    :Section Documentation Utilities
    ⍝ these are generic utilities used for documentation

    ∇ docn←ExtractDocumentationSections describeOnly;⎕IO;box;CR;sections;showMe;tit;z;docn∆
    ⍝ internal utility function
      ⎕IO←1
      CR←⎕UCS 13
      box←{{⍵{⎕AV[(1,⍵,1)/223 226 222],CR,⎕AV[231],⍺,⎕AV[231],CR,⎕AV[(1,⍵,1)/224 226 221]}⍴⍵}(⍵~CR),' '}
      docn←1↓⎕SRC ⎕THIS
      docn←1↓¨docn/⍨∧\'⍝'=⊃¨docn ⍝ keep all contiguous comments
      docn←docn/⍨'⍝'≠⊃¨docn     ⍝ remove any lines beginning with ⍝⍝
      sections←{∨/'::'⍷⍵}¨docn
      :If 11=⎕DR'describeOnly'
      :AndIf describeOnly
          (sections docn)←((2>+\sections)∘/¨sections docn)
      :ElseIf (⎕DR' ')=⎕DR'describeOnly'   ⍝ alternate usage: pass a string containing name of sections to show  (or a vtv of names)
          describeOnly←(819⌶)describeOnly
          tit←(819⌶)sections/docn
          docn∆←⍬
          :For showMe :In ,{1=≡⍵:⊂⍵ ⋄ ⍵}describeOnly
              :If ∨/z←∨/¨showMe∘⍷¨tit
                  docn∆,←((z⍳1)=+\sections)/docn
              :EndIf
          :EndFor
          sections←{∨/'::'⍷⍵}¨docn←docn∆
      :EndIf
      (sections/docn)←box¨sections/docn
      docn←∊docn,¨CR
    ∇

    ∇ r←Documentation
    ⍝ return full documentation
      :Access public shared
      r←ExtractDocumentationSections 0
    ∇

    ∇ r←Describe
    ⍝ return description only
      :Access public shared
      r←ExtractDocumentationSections 1
    ∇

    ∇ r←ShowDoc look4
    ⍝ only show section that contain argument in their title
      :Access public shared
      r←ExtractDocumentationSections look4
    ∇

    ∇ R←Version
      :Access public shared
      R←'Files' '1.0.0' '2017-08-08'
    ∇

    :EndSection
:EndNamespace


Allow each session to have its own timeout

:Class SimpleSessions
⍝ Default Session Handler, used by the WebServer sample
⍝ Manages HTTP sessions using cookies
⍝ When a HTTP request arrives, checks to see if the session cookie is included and the session
⍝ is in the list of valid sessions. If it is, assign the Session property of the request.
⍝ If it isn't, create a new session and set the cookie

    :Field Public Sessions ⍝ Should be private, really
    :Field Public Server

    :Class Page
        :Field Public URL←''
        :Field Public State←⎕NS ''
    :EndClass

    :Class Session
        :Field Public ID←0
        :Field Public User←''
        :Field Public LastActive←0
        :Field Public Cookie←''
        :Field Public AuthCookieName←''
        :Field Public Timeout←0
        :Field Public State
        :Field Public New←1
        :Field Public Pages←0⍴⎕NEW Page
        :Field Public Server←⍬
    :EndClass

    ∇ Start server;tn;root
      :Access Public
      :Implements Constructor
    ⍝ Initialize Session handler

      Sessions←0⍴⎕NEW Session
      root←server.Config.Root
      Timeout←server.Config.SessionTimeout
      timeout←Timeout÷24×60 ⍝ Convert minutes to fractions of a day

      :Trap 22
          tn←(root,'sessions.dcf')⎕FCREATE 0
          (0 ¯1 0)⎕FSTAC tn
          0 ⎕FAPPEND tn ⍝ Session Number
          ⎕FUNTIE tn
      :EndTrap
      tn←(root,'sessions.dcf')⎕FSTIE 0
      NextSession←⎕FREAD tn,1
      ⎕FUNTIE tn
      Server←server
    ∇

    ∇ GetSession req;c;tn;now;session;ns;new;t_out;i;r
      :Access Public
    ⍝ Return session. Right argument is a HTTPRequest.
      :If (1=Server.Config.CreateSession)∨'get'≢req.Method
          :Hold 'Sessions'
              session←req.GetCookie'Session'
              now←#.Dates.DateToIDN ⎕TS

              :If new←(1⊃⍴Sessions)<i←Sessions.Cookie⍳⊂session ⍝ Cookie is not in the table
                  c←SessionCookie NextSession
                  Sessions←Sessions,r←⎕NEW Session
                  r.(ID User LastActive Cookie State Server Timeout)←NextSession''now c(⎕NS'')Server timeout
                  NextSession←(2*30)|NextSession+1
                  tn←(req.Server.Config.Root,'sessions.dcf')⎕FSTIE 0
                  NextSession ⎕FREPLACE tn,1
                  ⎕FUNTIE tn
                  req.SetCookie'Session'c'/' 30 'HttpOnly'
                  req.Session←r
                  req.Server.onSessionStart req

              :Else ⍝ Old session
                  (r←i⊃Sessions).LastActive←now ⍝ Just register activity
                  r.New←0
                  req.Session←r
              :EndIf
          :EndHold
      :EndIf
    ∇

    ∇ KillSessions ids;mask;i
      :Access Public
      :Hold 'Sessions'
          :If ∨/mask←Sessions.ID∊ids
              :For i :In mask/⍳⍴mask
                  Server.onSessionEnd i⊃Sessions
              :EndFor
              Sessions/⍨←~mask
          :EndIf
      :EndHold
    ∇

    ∇ Logout req;session;i;z
      :Access Public Instance
     ⍝ End session due to user request

      session←req.GetCookie'Session'
      :If (1⊃⍴Sessions)≥i←Sessions.Cookie⍳⊂session ⍝ Cookie is in the table
          req.Server.onSessionEnd(i⊃Sessions)
          req.DelCookie'Session'
          :If 0≠⍴z←req.Session.AuthCookieName ⋄ req.DelCookie z ⋄ :EndIf
          req.Return'Logged out ...'
      :Else
          req.Return'No session established ...'
      :EndIf
    ∇

    ∇ r←SessionCookie n;⎕RL;z;seed;p
      ⍝ Create Cookie from session ID or vice versa

      :If (10|⎕DR n)∊0 2 ⍝ Session ID from Cookie
          z←⎕UCS #.Base64.Decode n
          ⎕RL←256⊥2↑z ⋄ p←30?30
          r←30⍴0 ⋄ r[p]←2↓z
          r←256⊥4↑r

      :Else ⍝ Session ID to Cookie
          ⎕RL←10000000|3⊃⎕AI
          ⎕RL←seed←?¯1+256*2 ⋄ p←30?30
          r←#.Base64.Encode #.Base64.Char(256 256⊤seed),((256 256 256 256⊤n),¯1+?26⍴256)[p]
      :EndIf
    ∇

    ∇ HouseKeeping Server;now;m;i;p
    ⍝ Check to see if any sessions have expired due to inactivity
    ⍝ Call any page application callbacks (_Close) if necessary

      :Access Public
      now←#.Dates.DateToIDN ⎕TS
      :If ∨/m←Sessions.LastActive<now-Sessions.Timeout
          :Hold 'Sessions'
              :For i :In m/⍳⍴m
                  Server.onSessionEnd i⊃Sessions
                  :If 0≠⍴p←(i⊃Sessions).Pages
                  :AndIf 0≠⊃p.⎕NC⊂'_Close'
                      p._Close i⊃Sessions
                  :EndIf
              :EndFor
              Sessions←(~m)/Sessions
          :EndHold
      :EndIf
    ∇

:EndClass

:Class SimpleSessions
⍝ Default Session Handler, used by the WebServer sample
⍝ Manages HTTP sessions using cookies
⍝ When a HTTP request arrives, checks to see if the session cookie is included and the session
⍝ is in the list of valid sessions. If it is, assign the Session property of the request.
⍝ If it isn't, create a new session and set the cookie

    :Field Public Sessions ⍝ Should be private, really
    :Field Public Server

    :Class Page
        :Field Public URL←''
        :Field Public State←⎕NS ''
    :EndClass

    :Class Session
        :Field Public ID←0
        :Field Public User←''
        :Field Public LastActive←0
        :Field Public Cookie←''
        :Field Public AuthCookieName←''
        :Field Public Timeout←0
        :Field Public State
        :Field Public New←1
        :Field Public Pages←0⍴⎕NEW Page
        :Field Public Server←⍬
    :EndClass

    ∇ Start server;tn;root
      :Access Public
      :Implements Constructor
    ⍝ Initialize Session handler

      Sessions←0⍴⎕NEW Session
      root←server.Config.Root
      Timeout←server.Config.SessionTimeout
      timeout←Timeout÷24×60 ⍝ Convert minutes to fractions of a day

      :Trap 22
          tn←(root,'sessions.dcf')⎕FCREATE 0
          (0 ¯1 0)⎕FSTAC tn
          0 ⎕FAPPEND tn ⍝ Session Number
          ⎕FUNTIE tn
      :EndTrap
      tn←(root,'sessions.dcf')⎕FSTIE 0
      NextSession←⎕FREAD tn,1
      ⎕FUNTIE tn
      Server←server
    ∇

    ∇ GetSession req;c;tn;now;session;ns;new;t_out;i;r
      :Access Public
    ⍝ Return session. Right argument is a HTTPRequest.
      :If (1=Server.Config.CreateSession)∨'get'≢req.Method
          :Hold 'Sessions'
              session←req.GetCookie'Session'
              now←#.Dates.DateToIDN ⎕TS

              :If new←(1⊃⍴Sessions)<i←Sessions.Cookie⍳⊂session ⍝ Cookie is not in the table
                  c←SessionCookie NextSession
                  Sessions←Sessions,r←⎕NEW Session
                  r.(ID User LastActive Cookie State Server)←NextSession''now c(⎕NS'')Server
                  NextSession←(2*30)|NextSession+1
                  tn←(req.Server.Config.Root,'sessions.dcf')⎕FSTIE 0
                  NextSession ⎕FREPLACE tn,1
                  ⎕FUNTIE tn
                  req.SetCookie'Session'c'/' 30 'HttpOnly'
                  req.Session←r
                  req.Server.onSessionStart req

              :Else ⍝ Old session
                  (r←i⊃Sessions).LastActive←now ⍝ Just register activity
                  r.New←0
                  req.Session←r
              :EndIf
          :EndHold
      :EndIf
    ∇

    ∇ KillSessions ids;mask;i
      :Access Public
      :Hold 'Sessions'
          :If ∨/mask←Sessions.ID∊ids
              :For i :In mask/⍳⍴mask
                  Server.onSessionEnd i⊃Sessions
              :EndFor
              Sessions/⍨←~mask
          :EndIf
      :EndHold
    ∇

    ∇ Logout req;session;i;z
      :Access Public Instance
     ⍝ End session due to user request

      session←req.GetCookie'Session'
      :If (1⊃⍴Sessions)≥i←Sessions.Cookie⍳⊂session ⍝ Cookie is in the table
          req.Server.onSessionEnd(i⊃Sessions)
          req.DelCookie'Session'
          :If 0≠⍴z←req.Session.AuthCookieName ⋄ req.DelCookie z ⋄ :EndIf
          req.Return'Logged out ...'
      :Else
          req.Return'No session established ...'
      :EndIf
    ∇

    ∇ r←SessionCookie n;⎕RL;z;seed;p
      ⍝ Create Cookie from session ID or vice versa

      :If (10|⎕DR n)∊0 2 ⍝ Session ID from Cookie
          z←⎕UCS #.Base64.Decode n
          ⎕RL←256⊥2↑z ⋄ p←30?30
          r←30⍴0 ⋄ r[p]←2↓z
          r←256⊥4↑r

      :Else ⍝ Session ID to Cookie
          ⎕RL←10000000|3⊃⎕AI
          ⎕RL←seed←?¯1+256*2 ⋄ p←30?30
          r←#.Base64.Encode #.Base64.Char(256 256⊤seed),((256 256 256 256⊤n),¯1+?26⍴256)[p]
      :EndIf
    ∇

    ∇ HouseKeeping Server;now;m;i;p
    ⍝ Check to see if any sessions have expired due to inactivity
    ⍝ Call any page application callbacks (_Close) if necessary

      :Access Public
      now←#.Dates.DateToIDN ⎕TS
      :If ∨/m←Sessions.LastActive<now-timeout
          :Hold 'Sessions'
              :For i :In m/⍳⍴m
                  Server.onSessionEnd i⊃Sessions
                  :If 0≠⍴p←(i⊃Sessions).Pages
                  :AndIf 0≠⊃p.⎕NC⊂'_Close'
                      p._Close i⊃Sessions
                  :EndIf
              :EndFor
              Sessions←(~m)/Sessions
          :EndHold
      :EndIf
    ∇

:EndClass


Workaround for ⎕FIX

Workaround for ⎕FIX

 output←regexes(op Run args)input;from;to;options;lines;Expand;repeat;s;r;last;fns;q;hasrepeat;postproc;combine;Quote;Nest;a;n
 output←⍬⊤⍬ ⍝ default result is empty matrix

 n←⎕UCS 10 ⍝ newline
 a←⎕UCS 39 ⍝ apostrofe
 ⍎'Nest←{⊂⍣(1=≡,⍵)⊢⍵}'
 ⍎'Quote←{a,a,⍨⍵/⍨1+⍵=a}'

 :Trap 11 22 ⍝ accept filename or actual input
     input←input ⎕NTIE 0
 :EndTrap

 :Trap 11 22 ⍝ accept filename or actual regex
     regexes←⊃⎕NGET regexes 1
 :EndTrap
 regexes←{,¨⍣(¯2=≡⍵)⊢⍵}Nest regexes

 repeat←⍎{×≢⍵:⍵ ⋄ '1'}args∩'≡',⎕D ⍝ extract repetition, if any, else 1
 hasrepeat←∨/args∊⍨'≡',⎕D
 ⍝ select options:
 options←('ResultText' 'Simple')('Greedy' 0)('IC' 1)('Mode' 'D')('Mode' 'M')('DotAll' 1)('UCP' 1)('OM' 1)/⍨1,'gidmauo'∊819⌶args~' -'
 s r←(∨/∊∘op)¨'Ss' 'Rr'
 :If s∨r
     options↓⍨←s ⍝ ResultText is not for ⎕S

     fns←+/∧\'⍵'∊¨regexes
     postproc←∊'⊢','∘{'∘,¨,∘'}'¨fns↑regexes ⍝ post-processing functions
     regexes↓⍨←fns

     last←⊃regexes↓⍨¯1+≢regexes
     :If '⍵'∊last ⍝ transformation function → many-to-function
     :OrIf 2|≢regexes ⍝ odd number of strings → many-to-one
         from←¯1↓regexes ⍝ remove transformation pattern/function
         :If ~'⍵'∊last ⍝ no ⍵ → pattern
             Expand←'⍵B' '⍵b' '⍵P' '⍵p' '⍵M' '⍵O' '⍵L' '⍵N'⎕R'⍵.Block' '⍵.BlockNum' '⍵.Pattern' '⍵.PatternNum' '⍵.Match' '⍵.Offsets' '⍵.Lengths' '⍵.Names'
         :OrIf ~≡⎕FX,⊂'to←{',(r/',⍕{'),(Expand last),(r/'}⍵'),'}' ⍝ if definition fails, revert to pattern (allows ⍵ in pattern)
             to←last
         :EndIf
     :Else
         lines←2÷⍨≢regexes
         from←regexes↓⍨-lines
         to←lines↓regexes
     :EndIf

     combine←'{⊃,/⍵↑¨⍨⌈/≢¨⍵}'
     q←⎕NS ⍬
     q.from←⍕Quote¨Nest from
     :If 0=≢q.from
         q.from←a a
     :EndIf
     :If 1∧.=≢¨from
         q.(from←'(,¨',from,')')
     :EndIf
     :If 3=⎕NC'to'
         q.to←1⌽'  ',3↓∊⎕NR'to'
     :Else
         q.to←⍕Quote¨Nest to
         :If 0=≢q.to
             q.to←a a
         :EndIf
         :If 1∧.=≢¨to
             q.(to←'(,¨',to,')')
         :EndIf
     :EndIf
     :If 3=⎕NC'repeat'
         q.repeat←'⍣',∊⎕NR'repeat'
     :ElseIf 1≢repeat
         q.repeat←'⍣',⍕repeat
     :Else
         q.repeat←''
     :EndIf
     :If ×≢options
         q.options←'⍠',∊{' (',(Quote⊃⍵),' ',({0 1∊⍨⊂⍵:⍕⍵ ⋄ Quote ⍵}⊃⌽⍵),')'}¨options
     :Else
         q.options←''
     :EndIf
     q.op←s r/'SR'
     q.postproc←2↓postproc
     q.postproc,←(hasrepeat∧s)/(''≡q.postproc)↓'∘',combine
     q.(expr←postproc,from,'⎕',op,to,options,repeat)
     :If '?'∊args
         ⍞←q.expr,n
     :EndIf

     :If 0=≢from
         from←''
     :EndIf
     :If 2=⎕NC'to'
     :AndIf 0=≢to
         to←''
     :EndIf

     :Trap 11
         :If r
             output←from ⎕R to⍠options⍣repeat⊢input
         :ElseIf s
             output←from ⎕S to⍠options⍣repeat⊢input
             output←(⍎combine)⍣hasrepeat⊢output ⍝ merge search results if we need to continue
         :EndIf
         output←(⍎postproc)output
     :Case 11 ⍝ faulty regex
         ⍞←'*** ',⎕DMX.(Message,(''≡Message)/⊃DM),' ***',n
         ⍞←'Attempted APL expression: ',q.expr,n
     :EndTrap
 :Else
     ⍞←'*** Left operand must be ''R'' or ''S'' ***'
 :EndIf

 output←regexes(op Run args)input;from;to;options;lines;Expand;repeat;s;r;last;fns;q;hasrepeat;postproc;combine;Quote;Nest;a;n
 output←⍬⊤⍬ ⍝ default result is empty matrix

 n←⎕UCS 10 ⍝ newline
 a←⎕UCS 39 ⍝ apostrofe
 Nest←{⊂⍣(1=≡,⍵)⊢⍵}
 Quote←{a,a,⍨⍵/⍨1+⍵=a}

 :Trap 11 22 ⍝ accept filename or actual input
     input←input ⎕NTIE 0
 :EndTrap

 :Trap 11 22 ⍝ accept filename or actual regex
     regexes←⊃⎕NGET regexes 1
 :EndTrap
 regexes←{,¨⍣(¯2=≡⍵)⊢⍵}Nest regexes

 repeat←⍎{×≢⍵:⍵ ⋄ '1'}args∩'≡',⎕D ⍝ extract repetition, if any, else 1
 hasrepeat←∨/args∊⍨'≡',⎕D
 ⍝ select options:
 options←('ResultText' 'Simple')('Greedy' 0)('IC' 1)('Mode' 'D')('Mode' 'M')('DotAll' 1)('UCP' 1)('OM' 1)/⍨1,'gidmauo'∊819⌶args~' -'
 s r←(∨/∊∘op)¨'Ss' 'Rr'
 :If s∨r
     options↓⍨←s ⍝ ResultText is not for ⎕S

     fns←+/∧\'⍵'∊¨regexes
     postproc←∊'⊢','∘{'∘,¨,∘'}'¨fns↑regexes ⍝ post-processing functions
     regexes↓⍨←fns

     last←⊃regexes↓⍨¯1+≢regexes
     :If '⍵'∊last ⍝ transformation function → many-to-function
     :OrIf 2|≢regexes ⍝ odd number of strings → many-to-one
         from←¯1↓regexes ⍝ remove transformation pattern/function
         :If ~'⍵'∊last ⍝ no ⍵ → pattern
             Expand←'⍵B' '⍵b' '⍵P' '⍵p' '⍵M' '⍵O' '⍵L' '⍵N'⎕R'⍵.Block' '⍵.BlockNum' '⍵.Pattern' '⍵.PatternNum' '⍵.Match' '⍵.Offsets' '⍵.Lengths' '⍵.Names'
         :OrIf ~≡⎕FX,⊂'to←{',(r/',⍕{'),(Expand last),(r/'}⍵'),'}' ⍝ if definition fails, revert to pattern (allows ⍵ in pattern)
             to←last
         :EndIf
     :Else
         lines←2÷⍨≢regexes
         from←regexes↓⍨-lines
         to←lines↓regexes
     :EndIf

     combine←'{⊃,/⍵↑¨⍨⌈/≢¨⍵}'
     q←⎕NS ⍬
     q.from←⍕Quote¨Nest from
     :If 0=≢q.from
         q.from←a a
     :EndIf
     :If 1∧.=≢¨from
         q.(from←'(,¨',from,')')
     :EndIf
     :If 3=⎕NC'to'
         q.to←1⌽'  ',3↓∊⎕NR'to'
     :Else
         q.to←⍕Quote¨Nest to
         :If 0=≢q.to
             q.to←a a
         :EndIf
         :If 1∧.=≢¨to
             q.(to←'(,¨',to,')')
         :EndIf
     :EndIf
     :If 3=⎕NC'repeat'
         q.repeat←'⍣',∊⎕NR'repeat'
     :ElseIf 1≢repeat
         q.repeat←'⍣',⍕repeat
     :Else
         q.repeat←''
     :EndIf
     :If ×≢options
         q.options←'⍠',∊{' (',(Quote⊃⍵),' ',({0 1∊⍨⊂⍵:⍕⍵ ⋄ Quote ⍵}⊃⌽⍵),')'}¨options
     :Else
         q.options←''
     :EndIf
     q.op←s r/'SR'
     q.postproc←2↓postproc
     q.postproc,←(hasrepeat∧s)/(''≡q.postproc)↓'∘',combine
     q.(expr←postproc,from,'⎕',op,to,options,repeat)
     :If '?'∊args
         ⍞←q.expr,n
     :EndIf

     :If 0=≢from
         from←''
     :EndIf
     :If 2=⎕NC'to'
     :AndIf 0=≢to
         to←''
     :EndIf

     :Trap 11
         :If r
             output←from ⎕R to⍠options⍣repeat⊢input
         :ElseIf s
             output←from ⎕S to⍠options⍣repeat⊢input
             output←(⍎combine)⍣hasrepeat⊢output ⍝ merge search results if we need to continue
         :EndIf
         output←(⍎postproc)output
     :Case 11 ⍝ faulty regex
         ⍞←'*** ',⎕DMX.(Message,(''≡Message)/⊃DM),' ***',n
         ⍞←'Attempted APL expression: ',q.expr,n
     :EndTrap
 :Else
     ⍞←'*** Left operand must be ''R'' or ''S'' ***'
 :EndIf


Fixed stupid typo in SeleniumTests

Fixed stupid typo in SeleniumTests

:Namespace SeleniumTests
⍝ needs Selenium/ in a folder that is on the same level as the DUI- or MiServer-Folder (ie /Git/MiServer & /Git/Selenium)

    ∇ x←eis x
⍝ Enclose if simple
      :If (≡x)∊0 1 ⋄ x←,⊂,x ⋄ :EndIf
    ∇

    ∇ r←lopFirst url
    ⍝ remove first segment of URL
      r←{⍵/⍨(1+'/'=1↑⍵)≤+\'/'=⍵}url
    ∇

    ∇ r←stop Run1Test page;name;ref;Test;pg
     ⍝ eg MS3Test '/QA/DC/InputGridSimple'
      :Trap 11
          Selenium.GoTo SITE,lopFirst page ⍝ Drop the "QA"
      :Else
          r←'Error while opening "',(lopFirst page),'": ',⎕DM
          →(stop=0)/0
          ∘∘∘
      :EndTrap
      :If 9=⎕NC'#.DUI' ⋄ pg←#.DUI.AppRoot,page ⋄ :Else ⋄ pg←AppRoot,page ⋄ :EndIf
      :If 'Test'≡name←⎕SE.SALT.Load pg
          :Trap stop×9999
              'Test'⎕STOP⍨1/⍨2=stop ⍝ stop on line 1 if stop=2
              :If stop⌊0≠⍴r←Test ⍬
                  ⎕←'test for ',page,' failed:' ⋄ ⎕←r ⋄ ⎕←'Rerun:' ⋄ '      Test ⍬'
                  ∘∘∘
              :EndIf
          :Else
              r←'Trapped error: ',,⍕⎕DMX.EN
          :EndTrap
      :Else ⋄ r←pg,' does not define a function called Test'
      :EndIf
    ∇

    ∇ r←{ext}FindAllFiles root;folders;ext
      :If 0=⎕NC'ext'
          :If 0=⎕NC'Config.DefaultExtension' ⋄ ext←'.dyalog'
          :Else ⋄ ext←Config.DefaultExtension
          :EndIf
      :EndIf
      root,←'/'/⍨~'/\'∊⍨¯1↑root ⍝ append trailing / if missing
      r←root∘,¨(('*',ext)#.Files.List root)[;1]
      :If 0≠⍴folders←{(('.'≠⊃¨⍵[;1])∧⍵[;4])/⍵[;1]}#.Files.List root
          r←r,⊃,/ext FindAllFiles¨root∘,¨folders
      :EndIf
    ∇

    ∇ r←stop_port Test site;count;ctl;examples;f;fail;nodot;start;t;time;z;i;START;COUNT;FAIL;Config;selpath;files;n;ext;filter;⎕PATH;keynames;maxlen;⎕USING;stopOnError;stop;dui;appr;cfg
      ⍝ stop: 0 (default) ignore but report errors; 1 stop on error; 2 stop before every test
      ⍝⍵: site filter config
      ⍝                config refers to a named entry in Selenium/settings.json
      stop←⊃stop_port
      r←''
      dui←9=⎕NC'#.DUI'

      (site filter config)←3↑(eis site),'' '' ''
      :If dui
          #.DUI.WC2Root←1⊃⎕NPARTS ¯1↓1⊃⎕NPARTS SALT_Data.SourceFile
          #.DUI.AppRoot←site
      :Else
          :If 0=⍴AppRoot←#.Load site
              ⎕←'Test abandoned' ⋄ →0
          :EndIf
          #.Boot.(DyalogRoot←folderize 2 ⎕NQ'.' 'GetEnvironment' 'DYALOG')
      :EndIf

     ⍝ selpath←({∊'/',⍨¨¯1↓'/\'#.Utils.penclose ⍵}#.Boot.MSRoot),'Selenium/'
      selpath←(1⊃⎕NPARTS ¯1↓1⊃⎕NPARTS ¯1↓1⊃⎕NPARTS SALT_Data.SourceFile),'Selenium/'
      :If 0=⎕NC'Selenium'
          :Trap 0 ⋄ ⎕SE.SALT.Load selpath,'Selenium'
          :Else
              ⎕←'Selenium library not found at: ',selpath
              →0⍴⍨0∊⍴selpath←{⍞↓⍨⍴⍞←⍵,': '}'Selenium path'
              :Trap 0 ⋄ ⎕SE.SALT.Load selpath,'Selenium'
              :Else ⋄ ⎕←'Unable to load Selenium' ⋄ →0
              :EndTrap
          :EndTrap
      :EndIf
      ⎕PATH,←' Selenium'
      Selenium.DLLPATH←selpath
      :If config≢''
          Selenium.ApplySettings config
      :Else
          ∘∘∘ ⍝ Can't run w/o config!
      :EndIf

      :If dui
          :If 0≠⊃z←#.DUI.Initialize
              ⎕←'Error initializing!' ⋄ ⎕←z
              ∘∘∘
          :EndIf
          appr←#.DUI.AppRoot
          cfg←#.Boot.ms.Config
      :Else
          Config←#.Boot.ConfigureServer AppRoot
          ext←Config.DefaultExtension
          Config.DefaultExtension←'.dyalog' ⍝ We are searching for code
          appr←AppRoot
          cfg←Config
      :EndIf

      n←⍴files←(⍴appr)↓¨¯7↓¨FindAllFiles appr,'QA'
      ⍝ // Add code to compare this to the mipages found in the whole app
      :If 0≠≢filter
          files←(filter ⎕S'%')files
          ⎕←'Selected: ',(⍕⍴files),' of ',(⍕n),' tests.'
      :EndIf
      n←⍴files
      ⍝SITE←'http://127.0.0.1:',⍕⊃1↓stop_port,Config.Port
      ⍝SITE←'http://',(2 ⎕NQ'.' 'TCPGetHostID'),':',(⍕{6::⍵.MSPort ⋄ ⍵.Port}#.Boot.ms.Config)
      ⎕←'Site=',SITE←'http://',(2 ⎕NQ'.' 'TCPGetHostID'),':',⍕⊃1↓stop_port,⍎⍕{6::⍵.MSPort ⋄ ⍵.Port}cfg

⍝⍝ Un-comment to play music while testing:
⍝      :If site filter≡'MS3' ''
⍝          ⎕CMD('"\Program Files (x86)\Windows Media Player\wmplayer.exe" "',AppRoot,'\Examples\Data\tellintro.mp3"')''
⍝      :EndIf

      Selenium.InitBrowser''

     ⍝ Localize non-alphanumeric key names for easy access
      keynames←⍕#.SeleniumTests.Selenium.Keys.⎕NL ¯2
      ⍎keynames,'←∊#.SeleniumTests.Selenium.Keys.(',keynames,')'

      START←⎕AI[3] ⋄ COUNT←0 ⋄ FAIL←0
      maxlen←¯2+⌈/⊃∘⍴¨files

      :For i :In ⍳n
          COUNT+←1
          :If 0=⍴t←stop Run1Test{⍵⊣⍞←(⎕UCS 13),maxlen↑lopFirst ⍵}z←i⊃files
              ⍞←' *** PASSED ***'
          :Else
              FAIL+←1
              r,←⊂z
              ⍞←' *** FAILED *** #',(⍕i),' of ',(⍕n),': ',z,': ',t
          :EndIf
      :EndFor

      ⎕←'Total of ',(⍕COUNT),' samples tested in ',(∊(⍕¨24 60⊤⌊0.5+(⎕AI[3]-START)÷1000),¨'ms'),': ',(⍕FAIL),' failed.'

      Selenium.BROWSER.Quit
    ∇

:EndNamespace

:Namespace SeleniumTests
⍝ needs Selenium/ in a folder that is on the same level as the DUI- or MiServer-Folder (ie /Git/MiServer & /Git/Selenium)

    ∇ x←eis x
⍝ Enclose if simple
      :If (≡x)∊0 1 ⋄ x←,⊂,x ⋄ :EndIf
    ∇

    ∇ r←lopFirst url
    ⍝ remove first segment of URL
      r←{⍵/⍨(1+'/'=1↑⍵)≤+\'/'=⍵}url
    ∇

    ∇ r←stop Run1Test page;name;ref;TestP;pg
     ⍝ eg MS3Test '/QA/DC/InputGridSimple'
      :Trap 11
          Selenium.GoTo SITE,lopFirst page ⍝ Drop the "QA"
      :Else
          r←'Error while opening "',(lopFirst page),'": ',⎕DM
          →(stop=0)/0
          ∘∘∘
      :EndTrap
      :If 9=⎕NC'#.DUI' ⋄ pg←#.DUI.AppRoot,page ⋄ :Else ⋄ pg←AppRoot,page ⋄ :EndIf
      :If 'Test'≡name←⎕SE.SALT.Load pg
          :Trap stop×9999
              'Test'⎕STOP⍨1/⍨2=stop ⍝ stop on line 1 if stop=2
              :If stop⌊0≠⍴r←Test ⍬
                  ⎕←'test for ',page,' failed:' ⋄ ⎕←r ⋄ ⎕←'Rerun:' ⋄ '      Test ⍬'
                  ∘∘∘
              :EndIf
          :Else
              r←'Trapped error: ',,⍕⎕DMX.EN
          :EndTrap
      :Else ⋄ r←pg,' does not define a function called Test'
      :EndIf
    ∇

    ∇ r←{ext}FindAllFiles root;folders;ext
      :If 0=⎕NC'ext'
          :If 0=⎕NC'Config.DefaultExtension' ⋄ ext←'.dyalog'
          :Else ⋄ ext←Config.DefaultExtension
          :EndIf
      :EndIf
      root,←'/'/⍨~'/\'∊⍨¯1↑root ⍝ append trailing / if missing
      r←root∘,¨(('*',ext)#.Files.List root)[;1]
      :If 0≠⍴folders←{(('.'≠⊃¨⍵[;1])∧⍵[;4])/⍵[;1]}#.Files.List root
          r←r,⊃,/ext FindAllFiles¨root∘,¨folders
      :EndIf
    ∇

    ∇ r←stop_port Test site;count;ctl;examples;f;fail;nodot;start;t;time;z;i;START;COUNT;FAIL;Config;selpath;files;n;ext;filter;⎕PATH;keynames;maxlen;⎕USING;stopOnError;stop;dui;appr;cfg
      ⍝ stop: 0 (default) ignore but report errors; 1 stop on error; 2 stop before every test
      ⍝⍵: site filter config
      ⍝                config refers to a named entry in Selenium/settings.json
      stop←⊃stop_port
      r←''
      dui←9=⎕NC'#.DUI'

      (site filter config)←3↑(eis site),'' '' ''
      :If dui
          #.DUI.WC2Root←1⊃⎕NPARTS ¯1↓1⊃⎕NPARTS SALT_Data.SourceFile
          #.DUI.AppRoot←site
      :Else
          :If 0=⍴AppRoot←#.Load site
              ⎕←'Test abandoned' ⋄ →0
          :EndIf
          #.Boot.(DyalogRoot←folderize 2 ⎕NQ'.' 'GetEnvironment' 'DYALOG')
      :EndIf

     ⍝ selpath←({∊'/',⍨¨¯1↓'/\'#.Utils.penclose ⍵}#.Boot.MSRoot),'Selenium/'
      selpath←(1⊃⎕NPARTS ¯1↓1⊃⎕NPARTS ¯1↓1⊃⎕NPARTS SALT_Data.SourceFile),'Selenium/'
      :If 0=⎕NC'Selenium'
          :Trap 0 ⋄ ⎕SE.SALT.Load selpath,'Selenium'
          :Else
              ⎕←'Selenium library not found at: ',selpath
              →0⍴⍨0∊⍴selpath←{⍞↓⍨⍴⍞←⍵,': '}'Selenium path'
              :Trap 0 ⋄ ⎕SE.SALT.Load selpath,'Selenium'
              :Else ⋄ ⎕←'Unable to load Selenium' ⋄ →0
              :EndTrap
          :EndTrap
      :EndIf
      ⎕PATH,←' Selenium'
      Selenium.DLLPATH←selpath
      :If config≢''
          Selenium.ApplySettings config
      :Else
          ∘∘∘ ⍝ Can't run w/o config!
      :EndIf

      :If dui
          :If 0≠⊃z←#.DUI.Initialize
              ⎕←'Error initializing!' ⋄ ⎕←z
              ∘∘∘
          :EndIf
          appr←#.DUI.AppRoot
          cfg←#.Boot.ms.Config
      :Else
          Config←#.Boot.ConfigureServer AppRoot
          ext←Config.DefaultExtension
          Config.DefaultExtension←'.dyalog' ⍝ We are searching for code
          appr←AppRoot
          cfg←Config
      :EndIf

      n←⍴files←(⍴appr)↓¨¯7↓¨FindAllFiles appr,'QA'
      ⍝ // Add code to compare this to the mipages found in the whole app
      :If 0≠≢filter
          files←(filter ⎕S'%')files
          ⎕←'Selected: ',(⍕⍴files),' of ',(⍕n),' tests.'
      :EndIf
      n←⍴files
      ⍝SITE←'http://127.0.0.1:',⍕⊃1↓stop_port,Config.Port
      ⍝SITE←'http://',(2 ⎕NQ'.' 'TCPGetHostID'),':',(⍕{6::⍵.MSPort ⋄ ⍵.Port}#.Boot.ms.Config)
      ⎕←'Site=',SITE←'http://',(2 ⎕NQ'.' 'TCPGetHostID'),':',⍕⊃1↓stop_port,⍎⍕{6::⍵.MSPort ⋄ ⍵.Port}cfg

⍝⍝ Un-comment to play music while testing:
⍝      :If site filter≡'MS3' ''
⍝          ⎕CMD('"\Program Files (x86)\Windows Media Player\wmplayer.exe" "',AppRoot,'\Examples\Data\tellintro.mp3"')''
⍝      :EndIf

      Selenium.InitBrowser''

     ⍝ Localize non-alphanumeric key names for easy access
      keynames←⍕#.SeleniumTests.Selenium.Keys.⎕NL ¯2
      ⍎keynames,'←∊#.SeleniumTests.Selenium.Keys.(',keynames,')'

      START←⎕AI[3] ⋄ COUNT←0 ⋄ FAIL←0
      maxlen←¯2+⌈/⊃∘⍴¨files

      :For i :In ⍳n
          COUNT+←1
          :If 0=⍴t←stop Run1Test{⍵⊣⍞←(⎕UCS 13),maxlen↑lopFirst ⍵}z←i⊃files
              ⍞←' *** PASSED ***'
          :Else
              FAIL+←1
              r,←⊂z
              ⍞←' *** FAILED *** #',(⍕i),' of ',(⍕n),': ',z,': ',t
          :EndIf
      :EndFor

      ⎕←'Total of ',(⍕COUNT),' samples tested in ',(∊(⍕¨24 60⊤⌊0.5+(⎕AI[3]-START)÷1000),¨'ms'),': ',(⍕FAIL),' failed.'

      Selenium.BROWSER.Quit
    ∇

:EndNamespace


Enclose simple data fields if from APLJax

:Class HTTPRequest

    (⎕IO ⎕ML)←1 1
    (CR LF)←NL←⎕UCS 13 10

⍝ Common Status Codes
    SC←(200 'OK')(201 'Created')(204 'No Content')(301 'Moved Permanently')(302 'Found')(303 'See Other')(304 'Not Modified')(305 'Use Proxy')(307 'Temporary Redirect')
    SC,←(400 'Bad Request')(401 'Unauthorized')(403 'Forbidden')(404 'Not Found')(405 'Method Not Allowed')(406 'Not Acceptable')(408 'Request Timeout')(409 'Conflict')
    SC,←(410 'Gone')(411 'Length Required')(412 'Precondition Failed')(413 'Request Entity Too Large')(414 'Request-URI Too Long')(415 'Unsupported Media Type')
    SC,←(500 'Internal Server Error')(501 'Not Implemented')(503 'Service Unavailable')
    SC←↑SC

⍝ Fields related to the Request

    :Field Public Instance Input
    :Field Public Instance Headers
    :Field Public Instance Command
    :Field Public Instance Page
    :Field Public Instance Filename
    :Field Public Instance RESTfulReq←0      ⍝ RESTful Request (set to charvec if request is RESTful)
    :Field Public Instance Arguments
    :Field Public Instance PeerAddr
    :Field Public Instance PeerCert
    :Field Public Instance Data
    :Field Public Instance Content←''
    :Field Public Instance Cookies
    :field Public Instance MSec
    :field Public Instance Bytes
    :Field Public Instance Session←''
    :Field Public Instance Server←⎕NS ''

⍝ Fields related to the Response

    :Field Public Instance Response

    GetFromTable←{(⍵[;1]⍳⊂#.Strings.lc ,⍺)⊃⍵[;2],⊂''}
    GetFromTableCS←{(⍵[;1]⍳⊂,⍺)⊃⍵[;2],⊂''} ⍝ Case Sensitive
    GetFromTableDefault←{⍺←'' ⋄ (⍺⍺[;1]⍳⊂,⍵)⊃⍺⍺[;2],⊂⍺} ⍝ default (table ∇) value
    ine←{0∊⍴⍺:'' ⋄ ⍵} ⍝ if not empty
    inf←{∨/⍵⍷⍺:'' ⋄ ⍵} ⍝ if not found
    enlist←{⎕ML←1 ⋄ ∊⍵}
    begins←{⍺≡(⍴⍺)↑⍵}
    split←{p←(⍺⍷⍵)⍳1 ⋄ ((p-1)↑⍵)(p↓⍵)} ⍝ Split ⍵ on first occurrence of ⍺
    sint←{⎕io←0 ⋄ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 ¯128 ¯127 ¯126 ¯125 ¯124 ¯123 ¯122 ¯121 ¯120 ¯119 ¯118 ¯117 ¯116 ¯115 ¯114 ¯113 ¯112 ¯111 ¯110 ¯109 ¯108 ¯107 ¯106 ¯105 ¯104 ¯103 ¯102 ¯101 ¯100 ¯99 ¯98 ¯97 ¯96 ¯95 ¯94 ¯93 ¯92 ¯91 ¯90 ¯89 ¯88 ¯87 ¯86 ¯85 ¯84 ¯83 ¯82 ¯81 ¯80 ¯79 ¯78 ¯77 ¯76 ¯75 ¯74 ¯73 ¯72 ¯71 ¯70 ¯69 ¯68 ¯67 ¯66 ¯65 ¯64 ¯63 ¯62 ¯61 ¯60 ¯59 ¯58 ¯57 ¯56 ¯55 ¯54 ¯53 ¯52 ¯51 ¯50 ¯49 ¯48 ¯47 ¯46 ¯45 ¯44 ¯43 ¯42 ¯41 ¯40 ¯39 ¯38 ¯37 ¯36 ¯35 ¯34 ¯33 ¯32 ¯31 ¯30 ¯29 ¯28 ¯27 ¯26 ¯25 ¯24 ¯23 ¯22 ¯21 ¯20 ¯19 ¯18 ¯17 ¯16 ¯15 ¯14 ¯13 ¯12 ¯11 ¯10 ¯9 ¯8 ¯7 ¯6 ¯5 ¯4 ¯3 ¯2 ¯1[⍵]}

    ∇ r←eis w
      :Access public shared
      r←{(,∘⊂)⍣((326∊⎕DR ⍵)<2>|≡⍵),⍵}w ⍝ Enclose if simple
    ∇

    ∇ Make(cmd data);buf;input;args;req;hdrs;i;z;pars;mask;new;s;cookies
      :Access Public Instance
      :Implements Constructor
⍝ Decode an HTTP command line: get /page&arg1=x&arg2=y
⍝ Return namespace containing:
⍝ Command: HTTP Command ('get' or 'post')
⍝ Headers: HTTP Headers as 2 column matrix or name/value pairs
⍝ Page:    Requested page
⍝ Arguments: Arguments to the command (cmd?arg1=value1&arg2=value2) as 2 column matrix of name/value pairs

      Response←⎕NS''
      Response.(Status StatusText Headers File HTML HTMLHead PeerAddr NoWrap Bytes)←200 'OK'(0 2⍴⊂'')0 '' '' '' 0(0 0)
      PeerCert←0 0⍴⊂'' ⋄ Data←0 2⍴⊂''
      PeerAddr←''
      MSec←⎕AI[3]

      input←1⊃,req←2⊃DecodeHeader cmd
      Input←input
      hdrs←{(0≠⊃∘⍴¨⍵[;1])⌿⍵}1 0↓req
      Headers←hdrs

      Command buf←' 'split input
      Command←#.Strings.lc Command

      buf z←'HTTP/'split buf
      Page args←'?'split ¯1↓buf
      Page←ArgXLT Page

      Arguments←URLDecodeArgs args

⍝ PeerCert←conns.PeerCert

      :If (1↑⍴hdrs)≥i←hdrs[;1]⍳⊂'content-type'
      :AndIf 'multipart/form-data'begins z←{(+/∧\⍵=' ')↓⍵}⊃hdrs[i;2]
          z←'UTF-8'⎕UCS'--',(8+('boundary='⍷z)⍳1)↓z ⍝ boundary string
          Data←↑DecodeMultiPart¨¯1↓z{(⍴⍺)↓¨(⍺⍷⍵)⊂⍵}data ⍝ ¯1↓ because last boundary has '--' appended
      :ElseIf 'application/x-www-form-urlencoded'begins z
          Data←URLDecodeArgs'UTF-8'⎕UCS data
      :ElseIf 'text/plain'begins z
          Data←1 2⍴'Data'('UTF-8'⎕UCS data) ⍝ if text, create artificial "Data" entry
      :Else
          Data←0 2⍴⊂''
          Content←'UTF-8'⎕UCS data
      :EndIf

      Cookies←0 2⍴⊂''
      :If (1↑⍴hdrs)≥i←hdrs[;1]⍳⊂'cookie'
          cookies←CookieSplit⊃hdrs[i;2]
          :If ~0∊⍴cookies←(2=⊃∘⍴¨cookies)/cookies
              Cookies←↑{((#.Strings.lc(1⊃⍵)~' '))(DeCode 2⊃⍵)}¨cookies
          :EndIf
      :EndIf

      :If ∨/mask←Data[;1]#.Strings.beginsWith¨⊂'_serialized_' ⍝ do we have any serialized form data from AJAX?
          new←0 2⍴⊂''
          :For s :In mask/Data[;2]
              new⍪←URLDecodeArgs s
          :EndFor
          Data←((~(⊃⍴Data)↑mask)⌿Data)⍪new
          Data←{0 1∊⍨⊃⍴⍵:⊃⍵ ⋄ ⍵}¨⊃{⍺ ⍵}#.Utils.∆key/↓[1]Data
      :EndIf

      :If ∨/mask←Data[;1]#.Strings.beginsWith¨⊂'_json_' ⍝ do we have any Syncfusion model data?
          :For s :In mask/⍳⍴mask
              Data[s;1]↓⍨←6
              Data[s;2]←eis #.JSON.toAPL⊃Data[s;2]
          :EndFor
      :EndIf
    ∇

    ∇ Wipe
      :Access public
    ⍝ clear out all request data
      Input←''
      Headers←''
      Command←''
      Page←''
      Filename←''
      RESTfulReq←0
      Arguments←''
      PeerAddr←''
      PeerCert←⍬
      Data←⍬
      Cookies←''
      MSec←⍬
      Bytes←⍬
      Session←''
      Server←⎕NS''
      Response←⎕NS''
    ∇

    :Section Argument and Data Handling

    ∇ r←ArgXLT r;rgx;rgxu;i;j;z;t;m;⎕IO;lens;fill
      :Access public shared
     ⍝ Translate HTTP command line arguments
      ⎕IO←0
      ((r='+')/r)←' '
      rgx←'[0-9a-fA-F]'
      rgxu←'%[uU]',(4×⍴rgx)⍴rgx ⍝ 4 characters
      r←(rgxu ⎕R{{⎕UCS 16⊥⍉16|'0123456789ABCDEF0123456789abcdef'⍳⍵}2↓⍵.Match})r
      :If 0≠⍴i←(r='%')/⍳⍴r
      :AndIf 0≠⍴i←(i≤¯2+⍴r)/i
          z←r[j←i∘.+1 2]
          t←'UTF-8'⎕UCS 16⊥⍉16|'0123456789ABCDEF0123456789abcdef'⍳z
          lens←⊃∘⍴¨'UTF-8'∘⎕UCS¨t  ⍝ UTF-8 is variable length encoding
          fill←i[¯1↓+\0,lens]
          r[fill]←t
          m←(⍴r)⍴1 ⋄ m[(,j),i~fill]←0
          r←m/r
      :EndIf
    ∇

    ∇ r←GetArgument name
      :Access Public Instance
      r←name GetFromTable Arguments
    ∇

    ∇ r←GetData name
      :Access Public Instance
      r←name GetFromTableCS Data
    ∇

    ∇ r←GetHeader name
      :Access Public Instance
      r←(#.Strings.lc name)GetFromTable Headers
    ∇

    ∇ r←{cs}URLDecodeArgs args
      :Access Public Shared
      cs←{6::⍵ ⋄ cs}1 ⍝ default to case sensitive
      r←(args∨.≠' ')⌿↑'='∘split¨{1↓¨(⍵='&')⊂⍵}'&',args ⍝ Cut on '&'
      r[;1]←{⍵↓⍨¯6×'%5B%5D'≡¯6↑⍵}¨r[;1] ⍝ remove [] from array args
      r[;2]←ArgXLT¨r[;2]
      :If ~cs ⋄ r[;1]←#.Strings.lc¨r[;1] ⋄ :EndIf
    ∇

    ∇ r←URLEncode data;⎕IO;z;ok;nul;m;enlist
      :Access Public Shared
      nul←⎕UCS ⎕IO←0
      enlist←{⎕ML←1 ⋄ ∊⍵}
      ok←nul,enlist ⎕UCS¨(⎕UCS'aA0')+⍳¨26 26 10

      z←⎕UCS'UTF-8'⎕UCS enlist nul,¨,data
      :If ∨/m←~z∊ok
          (m/z)←↓'%',(⎕D,⎕A)[⍉16 16⊤⎕UCS m/z]
          data←(⍴data)⍴1↓¨{(⍵=nul)⊂⍵}enlist z
      :EndIf

      r←¯1↓enlist data,¨(⍴data)⍴'=&'
    ∇

    ∇ r←DecodeHeader buf;len;d;i
 ⍝ Decode HTML Header
      :Access public shared
      len←(¯1+⍴NL,NL)+⊃{((NL,NL)⍷⍵)/⍳⍴⍵}buf
      :If len>0
          d←(⍴NL)↓¨{(NL⍷⍵)⊂⍵}NL,len↑buf
          d←↑{((p-1)↑⍵)({(' '=1↑⍵)↓⍵}(p←⍵⍳':')↓⍵)}¨d
          d[i;1]←#.Strings.lc¨d[i←1↓⍳1↑⍴d;1]
      :Else
          d←⍬
      :EndIf
      r←len d
    ∇

    ∇ r←DecodeMultiPart data;d;t;filename;name;i;hdr;ind
      hdr←'UTF-8'⎕UCS data↑⍨ind←1+1⍳⍨13 10 13 10⍷data
      d←'Content-Disposition: 'GetParam hdr

      name←filename←''
      :If (⍴d)≥i←5+('name="'⍷d)⍳1
          name←(¯1+name⍳'"')↑name←i↓d
      :EndIf

      :If (⍴d)≥i←9+('filename="'⍷d)⍳1
          filename←(¯1+filename⍳'"')↑filename←i↓d
      :EndIf

      data←(2+ind)↓data ⍝ Drop up to 1st doubleCR
      data←(¯1+¯1↑{⍵/⍳⍴⍵}13 10⍷data)↑data ⍝ Drop from last CR

      t←'Content-Type: 'GetParam hdr

      :If 0∊⍴filename
          :Select 5↑t ⍝ Content type
          :CaseList 'text/' '     ' ⍝ text formats
              :Trap 92 ⋄ data←'UTF-8'⎕UCS data ⋄ :EndTrap ⍝ From UTF-8
          :Else
          :EndSelect
      :Else
          data←sint data
          data←filename data
      :EndIf

      r←name data
    ∇

    ∇ r←header GetValue(name type);i;h
      ⍝ Extract value from HTTP Header structure returned by DecodeHeader

      :If (1↑⍴header)<i←header[;1]⍳⊂lc name
          r←⍬ ⍝ Not found
      :Else
          r←⊃header[i;2]
          :If 'Numeric'≡type
              r←1⊃2⊃⎕VFI r
          :EndIf
      :EndIf
    ∇

    ∇ r←name GetParam HTTP;i;z;find
      find←{⊃(⍺⍷⍵)/⍳⍴⍵}
      r←''
      :If 0<i←name find HTTP
      :AndIf 0<i←NL find z←(¯1+i+⍴name)↓HTTP
          r←(i-1)↑z
      :EndIf
    ∇

    :endsection

    :section Cookie Functions

    ∇ DelCookie ctl;name;path
      :Access Public Instance ⍝ Delete a cookie
      ctl←eis ctl
      name path←ctl,(⍴ctl)↓'CookieName' '/'
      SetCookie name''path ¯30 ⍝ Expiry 30 days ago
    ∇

    ∇ r←GetCookie name
      :Access Public Instance
      r←name GetFromTable Cookies
    ∇

    ∇ SetCookie ctl;name;value;path;date;z;keep
      :Access Public Instance ⍝ Set a Cookie
      ctl←eis ctl
      name value path keep←ctl,(⍴ctl)↓'CookieName' 'CookieValue' '/' 30
      date←#.Dates.IDNToDate keep+#.Dates.DateToIDN 3↑⎕TS ⍝ keep is # of days cookie should be valid
      z←#.Dates.CookieFmt(3↑date),23 59 59
      z←name,'=',(⍕value),'; path=',path,'; expires="',z,'"'
      Response.Headers⍪←'set-cookie'z
    ∇

    :endsection

    :section Request/Response Content Handling

    ∇ Fail x;i;root;f;t
      :Access Public Instance
      :If 3=10|⎕DR x ⍝ Just a status code
          Response.Status←x
          :If (1↑⍴SC[;1])≥i←SC[;1]⍳x
              Response.StatusText←⊃SC[i;2] ⍝ Default text
          :EndIf
      :Else
          Response.(Status StatusText)←x
      :EndIf
      :If 0≡RESTfulReq
          :For root :In Server.Config.(Root MSRoot) ⍝ try site root, then server root
              :If #.Files.Exists f←root,'CommonPages\',(⍕x),'.htm'
                  :If ~0∊⍴(⎕UCS 13 10)~⍨t←#.Files.GetText f
                      Response.HTML,⍨←t,'<br/>'
                      Response.Headers⍪←'content-type' 'text/html; charset=utf-8'
                  :EndIf
                  :Leave
              :EndIf
          :EndFor
      :EndIf
    ∇

    ∇ {code}Redirect location
      :Access public
      :If 0=⎕NC'code' ⋄ code←301 ⋄ :EndIf ⍝ default to permanent redirection
      Response.(Status StatusText)←code((SC[;1]⍳code)⊃SC[;2],⊂'')
      'Location'SetHeader location
    ∇

    ∇ r←isDesktop;cis;desktop;mobile;bot;user_agent ⍝ Detect if we think this is a desktop platform
⍝ Rationale: mobile device detection is a messy subject, therefore it's easier and perhaps safer to detect
⍝            desktop platforms (or bots) and assume that anything else is a mobile platform

      :Access public instance
      cis←{~0∊⍴(⍺ ⎕S 0 ⎕OPT'IC' 1)⍵} ⍝ Case Insensitive Search

      r←0

⍝ Some mobile browsers which look like desktop browsers.
      mobile←'(iphone|ipod|ipad|blackberry|android|palm|windows\s+ce)'
      desktop←'(windows|linux|os\s+[x9]|solaris|bsd)'
      bot←'(spider|crawl|slurp|bot)'

      →0⍴⍨0∊⍴user_agent←GetUserAgent
      →0⍴⍨mobile cis user_agent ⍝ Anything that looks like a phone isn't a desktop.
      →0⍴⍨r←desktop cis user_agent ⍝ Anything that looks like a desktop probably is.
      r←bot cis user_agent ⍝ Anything that looks like a bot should default to desktop.
    ∇

    ∇ r←GetUserAgent
      :Access public instance
      ⍝ Some mobile browsers put the User-Agent in a HTTP-X header
      →0↓⍨0∊⍴r←GetHeader'X-OPERAMINI-PHONE-UA'
      →0↓⍨0∊⍴r←GetHeader'X-SKYFIRE-PHONE'
      r←GetHeader'USER-AGENT'
    ∇

    ∇ r←isAPLJax
      :Access public instance
      r←'true'≡GetHeader'isAPLJax'
    ∇

    ∇ r←isPost
      :Access public instance
      r←Command≡'post'
    ∇

    ∇ r←isGet
      :Access public instance
      r←Command≡'get'
    ∇

    ∇ r←JSPlugIn file;root ⍝ Retrieve a JavaScript PlugIn
      :Access Public Instance
      root←{(1-⌊/'/\'⍳⍨⌽⍵)↓⍵}⎕WSID
      r←#.Files.GetText root,'PlugIns\',file
    ∇

    ∇ Meta attrs ⍝ add a meta tag to the head element
      :Access public instance
      Response.HTMLHead,←'<meta ',attrs,' />'
    ∇

    ∇ {hdrs}Return x;type
      :Access Public Instance

      :If 2=⍴⍴x ⋄ :AndIf 2=¯1↑⍴x ⋄ x←URLEncode x ⋄ type←'text/plain' ⍝ 2-column matrix is name/value pairs
      :Else ⋄ type←'text/html'
      :EndIf
      Response.HTML←enlist x
      :If 2=⎕NC'hdrs'
          Response.Headers⍪←hdrs
      :ElseIf ~∨/Response.Headers[;1](≡#.Strings.nocase)¨⊂'content-type'
          Response.Headers⍪←'content-type'(type,'; charset=utf-8') ⍝ set content-type if not already set
      :EndIf
    ∇

    ∇ {hdrs}ReturnFile x
      :Access Public Instance
      Response.(HTML File)←x 1
      :If 2=⎕NC'hdrs'
          Response.Headers⍪←hdrs
      :Else
          SetContentType x
      :EndIf
    ∇

    ∇ SetContentType x
      :Access public instance
      'content-type'SetHeader'application/binary'(Server.Config.ContentTypes GetFromTableDefault)#.Strings.lc{(1-(⌽⍵)⍳'.')↑⍵}x
    ∇

    ∇ {hdr}SetHeader value;val;mask
      :Access public instance
    ⍝ accepts value in forms
    ⍝ ('hdr1' 'val1')[('hdr2' 'val2')]
    ⍝ 'hdr1:val1' ['hdr2:val2']
    ⍝ 'hdr1' 'val1' ['hdr2' 'val2']
      :If 0=⎕NC'hdr'
          :If 3≡|≡value ⍝ pairs of strings
              val←↑value
          :ElseIf 2≡|≡value
              :If ':'∊¨value
                  val←↑':'split¨value
              :Else
                  val←((0.5×⍴value),2)⍴value
              :EndIf
          :Else
              val←1 2⍴':'split value
          :EndIf
      :Else
          (hdr value)←eis¨hdr value
          val←hdr,⍪value
      :EndIf
      :If ∨/mask←<\Response.Headers[;1](≡#.Strings.nocase)¨hdr
          (mask⌿Response.Headers)←val
      :Else
          Response.Headers⍪←val
      :EndIf
    ∇

    ∇ {tags}Script x
      :Access Public Instance
      :If 0=⎕NC'tags' ⋄ tags←'type="text/javascript"'
      :ElseIf ~∨/'type='⍷tags ⋄ tags,←' type="text/javascript"'
      :EndIf
      Response.HTMLHead,←Response.HTMLHead inf'<script ',tags,'>',(x ine NL,x),'</script>',NL
    ∇

    ∇ {style}Style file ⍝ insert a style into head element
      :Access Public Instance
      :If 0=⎕NC'style'
          :If ~0∊⍴file
              Response.HTMLHead,←Response.HTMLHead inf'<link href="',file,'" rel="stylesheet" type="text/css" />',NL
          :EndIf
      :Else
          :If 1<|≡style ⋄ style←NL,enlist(,∘NL)¨style ⋄ :EndIf
          Response.HTMLHead,←Response.HTMLHead inf'<style type="text/css">',style,'</style>',NL
      :EndIf
    ∇

    ∇ Title x
      :Access Public Instance
      Response.HTMLHead,←'<title>',x,'</title>',NL
    ∇

    ∇ Use x;n;ind
      :Access public instance
      :If 0≠⎕NC⊂'Server.Config.Resources'
      :AndIf ~0∊n←1↑⍴Server.Config.Resources
          :If n≥ind←Server.Config.Resources[;1]⍳⊂x
              (Script∘'')¨{'src="',⍵,'"'}¨{(⊃⍣(2<≡⍵))⍵}(⊂ind 2)⊃Server.Config.Resources
              Style¨{(⊃⍣(2<≡⍵))⍵}(⊂ind 3)⊃Server.Config.Resources
          :Else
              1 Server.Log Page,' references unknown resource: ',x
          :EndIf
      :EndIf
    ∇

    :endsection

      Split←{(⎕IO ⎕ML)←0 3         ⍝ Split Http syntax.
          ⍺←'?&=' ⋄ (,⍺){        ⍝ Default separator chars.
              0=⍴⍺:⍵             ⍝ No separator chars left: finished.
              sepr←↑⍺            ⍝ First char is separator char.
              (1↓⍺)∘∇∘{          ⍝ Recursively split at each sub level.
                  ⍵~sepr         ⍝ Separator removed.
              }¨(1+⍵=sepr)⊂⍵     ⍝ String partitioned at separator char.
          }⍵                     ⍝ Http string.
      }

      CookieSplit←{(⎕IO ⎕ML)←0 3   ⍝ Split cookies
          {{db←{⍵/⍨∨\⍵≠' '} ⋄ ⌽db⌽db ⍵}¨⍵⊂⍨~<\'='=⍵}¨⍵⊂⍨⍵≠';'}

      DeCode←{(⎕IO ⎕ML)←0 3          ⍝ Decode Special chars in HTML string.
          hex←'0123456789ABCDEF'     ⍝ Hex chars.
          {                          ⍝ Convert numbers.
              v f←⎕VFI ⍵             ⍝ Check for numbers.
              ~∧/v:⍵                 ⍝ Not all numbers: char vec.
              1=⍴f:↑f ⋄ f            ⍝ Numeric scalar or vector.
          }∊{                        ⍝ Enlist of segments.
              '%'≠↑⍵:⍵               ⍝ 1st seg may not contain special char.
              (⎕UCS 16⊥hex⍳1↓3↑⍵),3↓⍵  ⍝ Hex code replaced with corresp. ⎕AV char.
          }¨(1+⍵='%')⊂,⍵             ⍝ Segments split at '%' char.
      }

    ∇ Show
      :Access public
      ↑{⍵(⍎⍵)}¨'Input' 'Command' 'Page' 'Headers' 'Arguments' 'Data' 'Cookies'
    ∇

:EndClass

:Class HTTPRequest

    (⎕IO ⎕ML)←1 1
    (CR LF)←NL←⎕UCS 13 10

⍝ Common Status Codes
    SC←(200 'OK')(201 'Created')(204 'No Content')(301 'Moved Permanently')(302 'Found')(303 'See Other')(304 'Not Modified')(305 'Use Proxy')(307 'Temporary Redirect')
    SC,←(400 'Bad Request')(401 'Unauthorized')(403 'Forbidden')(404 'Not Found')(405 'Method Not Allowed')(406 'Not Acceptable')(408 'Request Timeout')(409 'Conflict')
    SC,←(410 'Gone')(411 'Length Required')(412 'Precondition Failed')(413 'Request Entity Too Large')(414 'Request-URI Too Long')(415 'Unsupported Media Type')
    SC,←(500 'Internal Server Error')(501 'Not Implemented')(503 'Service Unavailable')
    SC←↑SC

⍝ Fields related to the Request

    :Field Public Instance Input
    :Field Public Instance Headers
    :Field Public Instance Command
    :Field Public Instance Page
    :Field Public Instance Filename
    :Field Public Instance RESTfulReq←0      ⍝ RESTful Request (set to charvec if request is RESTful)
    :Field Public Instance Arguments
    :Field Public Instance PeerAddr
    :Field Public Instance PeerCert
    :Field Public Instance Data
    :Field Public Instance Content←''
    :Field Public Instance Cookies
    :field Public Instance MSec
    :field Public Instance Bytes
    :Field Public Instance Session←''
    :Field Public Instance Server←⎕NS ''

⍝ Fields related to the Response

    :Field Public Instance Response

    GetFromTable←{(⍵[;1]⍳⊂#.Strings.lc ,⍺)⊃⍵[;2],⊂''}
    GetFromTableCS←{(⍵[;1]⍳⊂,⍺)⊃⍵[;2],⊂''} ⍝ Case Sensitive
    GetFromTableDefault←{⍺←'' ⋄ (⍺⍺[;1]⍳⊂,⍵)⊃⍺⍺[;2],⊂⍺} ⍝ default (table ∇) value
    ine←{0∊⍴⍺:'' ⋄ ⍵} ⍝ if not empty
    inf←{∨/⍵⍷⍺:'' ⋄ ⍵} ⍝ if not found
    enlist←{⎕ML←1 ⋄ ∊⍵}
    begins←{⍺≡(⍴⍺)↑⍵}
    split←{p←(⍺⍷⍵)⍳1 ⋄ ((p-1)↑⍵)(p↓⍵)} ⍝ Split ⍵ on first occurrence of ⍺
    sint←{⎕io←0 ⋄ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 ¯128 ¯127 ¯126 ¯125 ¯124 ¯123 ¯122 ¯121 ¯120 ¯119 ¯118 ¯117 ¯116 ¯115 ¯114 ¯113 ¯112 ¯111 ¯110 ¯109 ¯108 ¯107 ¯106 ¯105 ¯104 ¯103 ¯102 ¯101 ¯100 ¯99 ¯98 ¯97 ¯96 ¯95 ¯94 ¯93 ¯92 ¯91 ¯90 ¯89 ¯88 ¯87 ¯86 ¯85 ¯84 ¯83 ¯82 ¯81 ¯80 ¯79 ¯78 ¯77 ¯76 ¯75 ¯74 ¯73 ¯72 ¯71 ¯70 ¯69 ¯68 ¯67 ¯66 ¯65 ¯64 ¯63 ¯62 ¯61 ¯60 ¯59 ¯58 ¯57 ¯56 ¯55 ¯54 ¯53 ¯52 ¯51 ¯50 ¯49 ¯48 ¯47 ¯46 ¯45 ¯44 ¯43 ¯42 ¯41 ¯40 ¯39 ¯38 ¯37 ¯36 ¯35 ¯34 ¯33 ¯32 ¯31 ¯30 ¯29 ¯28 ¯27 ¯26 ¯25 ¯24 ¯23 ¯22 ¯21 ¯20 ¯19 ¯18 ¯17 ¯16 ¯15 ¯14 ¯13 ¯12 ¯11 ¯10 ¯9 ¯8 ¯7 ¯6 ¯5 ¯4 ¯3 ¯2 ¯1[⍵]}

    ∇ r←eis w
      :Access public shared
      r←{(,∘⊂)⍣((326∊⎕DR ⍵)<2>|≡⍵),⍵}w ⍝ Enclose if simple
    ∇

    ∇ Make(cmd data);buf;input;args;req;hdrs;i;z;pars;mask;new;s;cookies
      :Access Public Instance
      :Implements Constructor
⍝ Decode an HTTP command line: get /page&arg1=x&arg2=y
⍝ Return namespace containing:
⍝ Command: HTTP Command ('get' or 'post')
⍝ Headers: HTTP Headers as 2 column matrix or name/value pairs
⍝ Page:    Requested page
⍝ Arguments: Arguments to the command (cmd?arg1=value1&arg2=value2) as 2 column matrix of name/value pairs

      Response←⎕NS''
      Response.(Status StatusText Headers File HTML HTMLHead PeerAddr NoWrap Bytes)←200 'OK'(0 2⍴⊂'')0 '' '' '' 0(0 0)
      PeerCert←0 0⍴⊂'' ⋄ Data←0 2⍴⊂''
      PeerAddr←''
      MSec←⎕AI[3]

      input←1⊃,req←2⊃DecodeHeader cmd
      Input←input
      hdrs←{(0≠⊃∘⍴¨⍵[;1])⌿⍵}1 0↓req
      Headers←hdrs

      Command buf←' 'split input
      Command←#.Strings.lc Command

      buf z←'HTTP/'split buf
      Page args←'?'split ¯1↓buf
      Page←ArgXLT Page

      Arguments←URLDecodeArgs args

⍝ PeerCert←conns.PeerCert

      :If (1↑⍴hdrs)≥i←hdrs[;1]⍳⊂'content-type'
      :AndIf 'multipart/form-data'begins z←{(+/∧\⍵=' ')↓⍵}⊃hdrs[i;2]
          z←'UTF-8'⎕UCS'--',(8+('boundary='⍷z)⍳1)↓z ⍝ boundary string
          Data←↑DecodeMultiPart¨¯1↓z{(⍴⍺)↓¨(⍺⍷⍵)⊂⍵}data ⍝ ¯1↓ because last boundary has '--' appended
      :ElseIf 'application/x-www-form-urlencoded'begins z
          Data←URLDecodeArgs'UTF-8'⎕UCS data
      :ElseIf 'text/plain'begins z
          Data←1 2⍴'Data'('UTF-8'⎕UCS data) ⍝ if text, create artificial "Data" entry
      :Else
          Data←0 2⍴⊂''
          Content←'UTF-8'⎕UCS data
      :EndIf

      Cookies←0 2⍴⊂''
      :If (1↑⍴hdrs)≥i←hdrs[;1]⍳⊂'cookie'
          cookies←CookieSplit⊃hdrs[i;2]
          :If ~0∊⍴cookies←(2=⊃∘⍴¨cookies)/cookies
              Cookies←↑{((#.Strings.lc(1⊃⍵)~' '))(DeCode 2⊃⍵)}¨cookies
          :EndIf
      :EndIf

      :If ∨/mask←Data[;1]#.Strings.beginsWith¨⊂'_serialized_' ⍝ do we have any serialized form data from AJAX?
          new←0 2⍴⊂''
          :For s :In mask/Data[;2]
              new⍪←URLDecodeArgs s
          :EndFor
          Data←((~(⊃⍴Data)↑mask)⌿Data)⍪new
          Data←{0 1∊⍨⊃⍴⍵:⊃⍵ ⋄ ⍵}¨⊃{⍺ ⍵}#.Utils.∆key/↓[1]Data
      :EndIf

      :If ∨/mask←Data[;1]#.Strings.beginsWith¨⊂'_json_' ⍝ do we have any Syncfusion model data?
          :For s :In mask/⍳⍴mask
              Data[s;1]↓⍨←6
              Data[s;2]←#.JSON.toAPL⊃Data[s;2]
          :EndFor
      :EndIf
    ∇

    ∇ Wipe
      :Access public
    ⍝ clear out all request data
      Input←''
      Headers←''
      Command←''
      Page←''
      Filename←''
      RESTfulReq←0
      Arguments←''
      PeerAddr←''
      PeerCert←⍬
      Data←⍬
      Cookies←''
      MSec←⍬
      Bytes←⍬
      Session←''
      Server←⎕NS''
      Response←⎕NS''
    ∇

    :Section Argument and Data Handling

    ∇ r←ArgXLT r;rgx;rgxu;i;j;z;t;m;⎕IO;lens;fill
      :Access public shared
     ⍝ Translate HTTP command line arguments
      ⎕IO←0
      ((r='+')/r)←' '
      rgx←'[0-9a-fA-F]'
      rgxu←'%[uU]',(4×⍴rgx)⍴rgx ⍝ 4 characters
      r←(rgxu ⎕R{{⎕UCS 16⊥⍉16|'0123456789ABCDEF0123456789abcdef'⍳⍵}2↓⍵.Match})r
      :If 0≠⍴i←(r='%')/⍳⍴r
      :AndIf 0≠⍴i←(i≤¯2+⍴r)/i
          z←r[j←i∘.+1 2]
          t←'UTF-8'⎕UCS 16⊥⍉16|'0123456789ABCDEF0123456789abcdef'⍳z
          lens←⊃∘⍴¨'UTF-8'∘⎕UCS¨t  ⍝ UTF-8 is variable length encoding
          fill←i[¯1↓+\0,lens]
          r[fill]←t
          m←(⍴r)⍴1 ⋄ m[(,j),i~fill]←0
          r←m/r
      :EndIf
    ∇

    ∇ r←GetArgument name
      :Access Public Instance
      r←name GetFromTable Arguments
    ∇

    ∇ r←GetData name
      :Access Public Instance
      r←name GetFromTableCS Data
    ∇

    ∇ r←GetHeader name
      :Access Public Instance
      r←(#.Strings.lc name)GetFromTable Headers
    ∇

    ∇ r←{cs}URLDecodeArgs args
      :Access Public Shared
      cs←{6::⍵ ⋄ cs}1 ⍝ default to case sensitive
      r←(args∨.≠' ')⌿↑'='∘split¨{1↓¨(⍵='&')⊂⍵}'&',args ⍝ Cut on '&'
      r[;1]←{⍵↓⍨¯6×'%5B%5D'≡¯6↑⍵}¨r[;1] ⍝ remove [] from array args
      r[;2]←ArgXLT¨r[;2]
      :If ~cs ⋄ r[;1]←#.Strings.lc¨r[;1] ⋄ :EndIf
    ∇

    ∇ r←URLEncode data;⎕IO;z;ok;nul;m;enlist
      :Access Public Shared
      nul←⎕UCS ⎕IO←0
      enlist←{⎕ML←1 ⋄ ∊⍵}
      ok←nul,enlist ⎕UCS¨(⎕UCS'aA0')+⍳¨26 26 10

      z←⎕UCS'UTF-8'⎕UCS enlist nul,¨,data
      :If ∨/m←~z∊ok
          (m/z)←↓'%',(⎕D,⎕A)[⍉16 16⊤⎕UCS m/z]
          data←(⍴data)⍴1↓¨{(⍵=nul)⊂⍵}enlist z
      :EndIf

      r←¯1↓enlist data,¨(⍴data)⍴'=&'
    ∇

    ∇ r←DecodeHeader buf;len;d;i
 ⍝ Decode HTML Header
      :Access public shared
      len←(¯1+⍴NL,NL)+⊃{((NL,NL)⍷⍵)/⍳⍴⍵}buf
      :If len>0
          d←(⍴NL)↓¨{(NL⍷⍵)⊂⍵}NL,len↑buf
          d←↑{((p-1)↑⍵)({(' '=1↑⍵)↓⍵}(p←⍵⍳':')↓⍵)}¨d
          d[i;1]←#.Strings.lc¨d[i←1↓⍳1↑⍴d;1]
      :Else
          d←⍬
      :EndIf
      r←len d
    ∇

    ∇ r←DecodeMultiPart data;d;t;filename;name;i;hdr;ind
      hdr←'UTF-8'⎕UCS data↑⍨ind←1+1⍳⍨13 10 13 10⍷data
      d←'Content-Disposition: 'GetParam hdr

      name←filename←''
      :If (⍴d)≥i←5+('name="'⍷d)⍳1
          name←(¯1+name⍳'"')↑name←i↓d
      :EndIf

      :If (⍴d)≥i←9+('filename="'⍷d)⍳1
          filename←(¯1+filename⍳'"')↑filename←i↓d
      :EndIf

      data←(2+ind)↓data ⍝ Drop up to 1st doubleCR
      data←(¯1+¯1↑{⍵/⍳⍴⍵}13 10⍷data)↑data ⍝ Drop from last CR

      t←'Content-Type: 'GetParam hdr

      :If 0∊⍴filename
          :Select 5↑t ⍝ Content type
          :CaseList 'text/' '     ' ⍝ text formats
              :Trap 92 ⋄ data←'UTF-8'⎕UCS data ⋄ :EndTrap ⍝ From UTF-8
          :Else
          :EndSelect
      :Else
          data←sint data
          data←filename data
      :EndIf

      r←name data
    ∇

    ∇ r←header GetValue(name type);i;h
      ⍝ Extract value from HTTP Header structure returned by DecodeHeader

      :If (1↑⍴header)<i←header[;1]⍳⊂lc name
          r←⍬ ⍝ Not found
      :Else
          r←⊃header[i;2]
          :If 'Numeric'≡type
              r←1⊃2⊃⎕VFI r
          :EndIf
      :EndIf
    ∇

    ∇ r←name GetParam HTTP;i;z;find
      find←{⊃(⍺⍷⍵)/⍳⍴⍵}
      r←''
      :If 0<i←name find HTTP
      :AndIf 0<i←NL find z←(¯1+i+⍴name)↓HTTP
          r←(i-1)↑z
      :EndIf
    ∇

    :endsection

    :section Cookie Functions

    ∇ DelCookie ctl;name;path
      :Access Public Instance ⍝ Delete a cookie
      ctl←eis ctl
      name path←ctl,(⍴ctl)↓'CookieName' '/'
      SetCookie name''path ¯30 ⍝ Expiry 30 days ago
    ∇

    ∇ r←GetCookie name
      :Access Public Instance
      r←name GetFromTable Cookies
    ∇

    ∇ SetCookie ctl;name;value;path;date;z;keep
      :Access Public Instance ⍝ Set a Cookie
      ctl←eis ctl
      name value path keep←ctl,(⍴ctl)↓'CookieName' 'CookieValue' '/' 30
      date←#.Dates.IDNToDate keep+#.Dates.DateToIDN 3↑⎕TS ⍝ keep is # of days cookie should be valid
      z←#.Dates.CookieFmt(3↑date),23 59 59
      z←name,'=',(⍕value),'; path=',path,'; expires="',z,'"'
      Response.Headers⍪←'set-cookie'z
    ∇

    :endsection

    :section Request/Response Content Handling

    ∇ Fail x;i;root;f;t
      :Access Public Instance
      :If 3=10|⎕DR x ⍝ Just a status code
          Response.Status←x
          :If (1↑⍴SC[;1])≥i←SC[;1]⍳x
              Response.StatusText←⊃SC[i;2] ⍝ Default text
          :EndIf
      :Else
          Response.(Status StatusText)←x
      :EndIf
      :If 0≡RESTfulReq
          :For root :In Server.Config.(Root MSRoot) ⍝ try site root, then server root
              :If #.Files.Exists f←root,'CommonPages\',(⍕x),'.htm'
                  :If ~0∊⍴(⎕UCS 13 10)~⍨t←#.Files.GetText f
                      Response.HTML,⍨←t,'<br/>'
                      Response.Headers⍪←'content-type' 'text/html; charset=utf-8'
                  :EndIf
                  :Leave
              :EndIf
          :EndFor
      :EndIf
    ∇

    ∇ {code}Redirect location
      :Access public
      :If 0=⎕NC'code' ⋄ code←301 ⋄ :EndIf ⍝ default to permanent redirection
      Response.(Status StatusText)←code((SC[;1]⍳code)⊃SC[;2],⊂'')
      'Location'SetHeader location
    ∇

    ∇ r←isDesktop;cis;desktop;mobile;bot;user_agent ⍝ Detect if we think this is a desktop platform
⍝ Rationale: mobile device detection is a messy subject, therefore it's easier and perhaps safer to detect
⍝            desktop platforms (or bots) and assume that anything else is a mobile platform

      :Access public instance
      cis←{~0∊⍴(⍺ ⎕S 0 ⎕OPT'IC' 1)⍵} ⍝ Case Insensitive Search

      r←0

⍝ Some mobile browsers which look like desktop browsers.
      mobile←'(iphone|ipod|ipad|blackberry|android|palm|windows\s+ce)'
      desktop←'(windows|linux|os\s+[x9]|solaris|bsd)'
      bot←'(spider|crawl|slurp|bot)'

      →0⍴⍨0∊⍴user_agent←GetUserAgent
      →0⍴⍨mobile cis user_agent ⍝ Anything that looks like a phone isn't a desktop.
      →0⍴⍨r←desktop cis user_agent ⍝ Anything that looks like a desktop probably is.
      r←bot cis user_agent ⍝ Anything that looks like a bot should default to desktop.
    ∇

    ∇ r←GetUserAgent
      :Access public instance
      ⍝ Some mobile browsers put the User-Agent in a HTTP-X header
      →0↓⍨0∊⍴r←GetHeader'X-OPERAMINI-PHONE-UA'
      →0↓⍨0∊⍴r←GetHeader'X-SKYFIRE-PHONE'
      r←GetHeader'USER-AGENT'
    ∇

    ∇ r←isAPLJax
      :Access public instance
      r←'true'≡GetHeader'isAPLJax'
    ∇

    ∇ r←isPost
      :Access public instance
      r←Command≡'post'
    ∇

    ∇ r←isGet
      :Access public instance
      r←Command≡'get'
    ∇

    ∇ r←JSPlugIn file;root ⍝ Retrieve a JavaScript PlugIn
      :Access Public Instance
      root←{(1-⌊/'/\'⍳⍨⌽⍵)↓⍵}⎕WSID
      r←#.Files.GetText root,'PlugIns\',file
    ∇

    ∇ Meta attrs ⍝ add a meta tag to the head element
      :Access public instance
      Response.HTMLHead,←'<meta ',attrs,' />'
    ∇

    ∇ {hdrs}Return x;type
      :Access Public Instance

      :If 2=⍴⍴x ⋄ :AndIf 2=¯1↑⍴x ⋄ x←URLEncode x ⋄ type←'text/plain' ⍝ 2-column matrix is name/value pairs
      :Else ⋄ type←'text/html'
      :EndIf
      Response.HTML←enlist x
      :If 2=⎕NC'hdrs'
          Response.Headers⍪←hdrs
      :ElseIf ~∨/Response.Headers[;1](≡#.Strings.nocase)¨⊂'content-type'
          Response.Headers⍪←'content-type'(type,'; charset=utf-8') ⍝ set content-type if not already set
      :EndIf
    ∇

    ∇ {hdrs}ReturnFile x
      :Access Public Instance
      Response.(HTML File)←x 1
      :If 2=⎕NC'hdrs'
          Response.Headers⍪←hdrs
      :Else
          SetContentType x
      :EndIf
    ∇

    ∇ SetContentType x
      :Access public instance
      'content-type'SetHeader'application/binary'(Server.Config.ContentTypes GetFromTableDefault)#.Strings.lc{(1-(⌽⍵)⍳'.')↑⍵}x
    ∇

    ∇ {hdr}SetHeader value;val;mask
      :Access public instance
    ⍝ accepts value in forms
    ⍝ ('hdr1' 'val1')[('hdr2' 'val2')]
    ⍝ 'hdr1:val1' ['hdr2:val2']
    ⍝ 'hdr1' 'val1' ['hdr2' 'val2']
      :If 0=⎕NC'hdr'
          :If 3≡|≡value ⍝ pairs of strings
              val←↑value
          :ElseIf 2≡|≡value
              :If ':'∊¨value
                  val←↑':'split¨value
              :Else
                  val←((0.5×⍴value),2)⍴value
              :EndIf
          :Else
              val←1 2⍴':'split value
          :EndIf
      :Else
          (hdr value)←eis¨hdr value
          val←hdr,⍪value
      :EndIf
      :If ∨/mask←<\Response.Headers[;1](≡#.Strings.nocase)¨hdr
          (mask⌿Response.Headers)←val
      :Else
          Response.Headers⍪←val
      :EndIf
    ∇

    ∇ {tags}Script x
      :Access Public Instance
      :If 0=⎕NC'tags' ⋄ tags←'type="text/javascript"'
      :ElseIf ~∨/'type='⍷tags ⋄ tags,←' type="text/javascript"'
      :EndIf
      Response.HTMLHead,←Response.HTMLHead inf'<script ',tags,'>',(x ine NL,x),'</script>',NL
    ∇

    ∇ {style}Style file ⍝ insert a style into head element
      :Access Public Instance
      :If 0=⎕NC'style'
          :If ~0∊⍴file
              Response.HTMLHead,←Response.HTMLHead inf'<link href="',file,'" rel="stylesheet" type="text/css" />',NL
          :EndIf
      :Else
          :If 1<|≡style ⋄ style←NL,enlist(,∘NL)¨style ⋄ :EndIf
          Response.HTMLHead,←Response.HTMLHead inf'<style type="text/css">',style,'</style>',NL
      :EndIf
    ∇

    ∇ Title x
      :Access Public Instance
      Response.HTMLHead,←'<title>',x,'</title>',NL
    ∇

    ∇ Use x;n;ind
      :Access public instance
      :If 0≠⎕NC⊂'Server.Config.Resources'
      :AndIf ~0∊n←1↑⍴Server.Config.Resources
          :If n≥ind←Server.Config.Resources[;1]⍳⊂x
              (Script∘'')¨{'src="',⍵,'"'}¨{(⊃⍣(2<≡⍵))⍵}(⊂ind 2)⊃Server.Config.Resources
              Style¨{(⊃⍣(2<≡⍵))⍵}(⊂ind 3)⊃Server.Config.Resources
          :Else
              1 Server.Log Page,' references unknown resource: ',x
          :EndIf
      :EndIf
    ∇

    :endsection

      Split←{(⎕IO ⎕ML)←0 3         ⍝ Split Http syntax.
          ⍺←'?&=' ⋄ (,⍺){        ⍝ Default separator chars.
              0=⍴⍺:⍵             ⍝ No separator chars left: finished.
              sepr←↑⍺            ⍝ First char is separator char.
              (1↓⍺)∘∇∘{          ⍝ Recursively split at each sub level.
                  ⍵~sepr         ⍝ Separator removed.
              }¨(1+⍵=sepr)⊂⍵     ⍝ String partitioned at separator char.
          }⍵                     ⍝ Http string.
      }

      CookieSplit←{(⎕IO ⎕ML)←0 3   ⍝ Split cookies
          {{db←{⍵/⍨∨\⍵≠' '} ⋄ ⌽db⌽db ⍵}¨⍵⊂⍨~<\'='=⍵}¨⍵⊂⍨⍵≠';'}

      DeCode←{(⎕IO ⎕ML)←0 3          ⍝ Decode Special chars in HTML string.
          hex←'0123456789ABCDEF'     ⍝ Hex chars.
          {                          ⍝ Convert numbers.
              v f←⎕VFI ⍵             ⍝ Check for numbers.
              ~∧/v:⍵                 ⍝ Not all numbers: char vec.
              1=⍴f:↑f ⋄ f            ⍝ Numeric scalar or vector.
          }∊{                        ⍝ Enlist of segments.
              '%'≠↑⍵:⍵               ⍝ 1st seg may not contain special char.
              (⎕UCS 16⊥hex⍳1↓3↑⍵),3↓⍵  ⍝ Hex code replaced with corresp. ⎕AV char.
          }¨(1+⍵='%')⊂,⍵             ⍝ Segments split at '%' char.
      }

    ∇ Show
      :Access public
      ↑{⍵(⍎⍵)}¨'Input' 'Command' 'Page' 'Headers' 'Arguments' 'Data' 'Cookies'
    ∇

:EndClass


UlAdvanced QA retries mouseover test now that utils can be used with Retry.

 msg←Test dummy;link;output
 :If 0∊⍴msg←'list not found'/⍨0≡link←('CssSelectors'Find'#links li')[1]
     output←Find'output'
     msg←'Mouseover test failed'/⍨~{∨/'new'⍷output.Text⊣MoveToElement link 10 10}Retry ⍬
 :EndIf

 msg←Test dummy;link
 :If 0∊⍴msg←'list not found'/⍨0≡link←('CssSelectors'Find'#links li')[1]
     MoveToElement link 10 10
     msg←'output'WaitFor'this' 'Mouseover test failed'
 :EndIf