Check Installed Plugins Against the Store.fh_lua

--[[
@Title: Check Installed Plugins Against The Store
@Author: Calico Pie
@Version: 3.3
@LastUpdated: 05 Oct 2022
@Description: Checks currently installed plugins against those currently in the store and optionally downloads and installs later versions.


V2.0 Added Author Column and updated to show the status of all plugins in the main plug directory.  
V2.1 Add Superseded support
V2.2 Improve version checking where the component numbers are greater than 9
V2.3/2.4 trim line endings.
V2.7/8 Improve Reporting for plugins not in store.
V2.9 Support HTTPS as now used at Family Historian when used with FH 7
V3.0 Lua 5.3 compatibility
V3.1 Support http for V6
V3.2 Strip hex(0) if returned in Status string
V3.3 Add Window handling for FH7 and above
@Licence: This plugin is copyright (c) 2022 Calico Pie & contributors and is licensed under the MIT License
which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)

]]
require('luacom')
require('lfs')
require('iuplua')

iup.SetGlobal("CUSTOMQUITMESSAGE","YES")
local sFHVer =  fhGetAppVersion()
local sHandle
siteurl = "https://www.family-historian.co.uk/lnk/"
if _VERSION == 'Lua 5.1' then
   siteurl = "http://www.family-historian.co.uk/lnk/"
end

bforcecheck = false

function main()
    local tblPlugin = {}
    local tblCurVer = {}
    local tblStoreVer = {}
    local tblStatusDesc = {}
    local tblAuthor = {}
    local tblMsg = {}
    local tblCount = {0,0,0,0,0,0,0}
    local tblStatus = {'A. Newer Than Store','B. Upgraded', 'C. New Version Available',
                       'D. Up To Date','E. Not In Store','F. No Version','G. Superseded'}
    pluginDir = fhGetContextInfo('CI_APP_DATA_FOLDER')..'\\plugins\\'
    local count = 0
    for filename,attr in dirtree(pluginDir) do
        path,name,ext = splitfilename(filename)
        if ext == 'fh_lua' and path == pluginDir then   -- Only Look at plugin files which are in the main plugin directory            
           count = count + 1
        end
    end
    local step = 100 / count
    ProgressBar.Start("Check Installed Plugins",count)
    for filename,attr in dirtree(pluginDir) do
        path,name,ext = splitfilename(filename)
        if ext == 'fh_lua' and path == pluginDir then   -- Only Look at plugin files which are in the main plugin directory            
            iStatus = 6
            ProgressBar.Step(step)
            if ProgressBar.Stop() then break end
            -- Get version information from script
            local script = StrLoadFromFile(filename)
            local _,_,version = string.find(script:lower(), '@version[:%s]%W+([%d%.]*)')
            local author= script:match('@[Aa]uthor:%s+([^\n]+)')
            local versionStore,id = nil,nil
            if bforcecheck then version = version or '0' end
            if version then
                versionStore,id,msg = checkVersionInStore('file',name)
                if version ~= versionStore then
                    if versionStore ~= nil and cvtVer(version) < cvtVer(versionStore) then
                        local res
							  if sFHVer >= 7 then
                            res = fhMessageBox('Newer version of '..name:gsub('%.fh_lua','')..' found, would you like to upgrade','MB_YESNO','MB_ICONQUESTION',sHandle)
							  else
                            res = fhMessageBox('Newer version of '..name:gsub('%.fh_lua','')..' found, would you like to upgrade','MB_YESNO','MB_ICONQUESTION')
							  end	 
                        if res == 'Yes' then
                            updatePlugin(id)
                            iStatus = 2
                        else
                            iStatus = 3
                        end
                    end
                end
                if iStatus == 6 then
                if versionStore and version == versionStore then
                    iStatus = 4
                else
                    if versionStore and cvtVer(version) > cvtVer(versionStore) then
                        iStatus = 1
                    else
                        iStatus = 5
                    end
                end
                end
            end
            if msg then
              -- fhMessageBox(name:gsub('%.fh_lua','')..' '..msg)
					if msg:find('not found') then 
                  iStatus = 5 
               else
                  fhMessageBox(name:gsub('%.fh_lua','')..' '..msg)
                  iStatus = 7 
                end
            end
            table.insert(tblPlugin,name:match('(.+)%.fh_lua'))
            table.insert(tblCurVer,version or ' ')
            table.insert(tblStoreVer,versionStore or ' ')
            table.insert(tblStatusDesc,tblStatus[iStatus] or ' ')
            table.insert(tblAuthor,author or ' ')
            table.insert(tblMsg,msg or ' ')
            tblCount[iStatus] = tblCount[iStatus] + 1
        end
    end
    ProgressBar.Close()
    if #tblPlugin > 0 then
        local strMsg = string.format('%u Plugins have been checked with the following Results:\n',sumtbl(tblCount))
        for i,v in ipairs(tblCount) do
            if v > 0 then
                strMsg = string.format('%s%s: %u \n',strMsg,tblStatus[i],v)
            end
        end
        strMsg = string.format('%s\nDisplay detailed results?\n',strMsg)
        local res = fhMessageBox(strMsg,'MB_YESNO','MB_ICONQUESTION')
        if res == 'Yes' then
            fhOutputResultSetTitles("Plug Status Summary", "Plug Status Summary", "Printed Date: %#x")
            -- Output Result Set
            fhOutputResultSetColumn("Status", "text", tblStatusDesc, #tblPlugin, 80, "align_left",1,true)
            fhOutputResultSetColumn("Current", "text", tblCurVer, #tblPlugin, 30, "align_right")
            fhOutputResultSetColumn("Store", "text", tblStoreVer, #tblPlugin, 25, "align_right")
            fhOutputResultSetColumn("Plugin", "text", tblPlugin, #tblPlugin, 250, "align_left",2,true)
            fhOutputResultSetColumn("Author", "text", tblAuthor, #tblPlugin, 150, "align_left",2,true)
            fhOutputResultSetColumn("Message", "text", tblMsg, #tblPlugin, 250, "align_left",2,true)
        end
    else
        fhMessageBox('No Plugins Found')
    end
end
-------------------------------------------------- Functions
function updatePlugin(id)
    -- Get Plugin down and upgrade it
    local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
    local url = siteurl.."fetchlatestpluginversion.php?id="..id
    http:Open("GET",url,false)
    http:Send()
    http:WaitForResponse(30)
    local status = stripz(http.StatusText)
    print('Status = '..status)
    print(http:getAllResponseHeaders())
if status == 'Moved Temporarily' then
	url = http:GetResponseHeader('Location')
   print(url)
   http:Open("GET",url,false)
    http:Send()
    http:WaitForResponse(30)
    status = stripz(http.StatusText)
    print('Status = '..status)
end
    if status == 'OK' then
        -- length = http:GetResponseHeader('Content-Length')
        print(http:GetResponseHeader('Content-Disposition'))
        filename = string.match(http:GetResponseHeader('Content-Disposition'),'="(.*)"')
        print(filename)
        data = http.ResponseBody
        data = data:gsub("\r\n","\n")  -- Remove CR characters at end of line
        SaveStringToFile(data,pluginDir..'\\'..filename)
        print('Data Size :'..#data)
        -- print('Content-Length:'..length)
        print(http:getAllResponseHeaders())
    else
        fhMessageBox('An error occurred in Download please try later')
    end
end
function checkVersionInStore(type,value)
--[[
@description: Checks the version in the plugin store by name or id
@Parms:
1: String, must be either 'id' or 'name' or file
2: String where name search or number where id search
@Returns:
1: nil where value not found or parameter 1 is invalid, or the value of the plugins version field
2: the reference
3: where an error was returned the message following the "Error:"
]]
    if type ~= 'id' and type ~= 'name' and type ~= 'file' then
        return nil
    end
    if value then
        local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
        fhVer = table.concat({fhGetAppVersion()},'.')
        strRequest =siteurl..'checkpluginversion.php?'..type..'='..value..'&fhver='..fhVer
        
        http:Open("GET",strRequest,false)
        http:Send()
        local strReturn = http.Responsebody
        print(strReturn)
        local ver,ref,msg 
        if strReturn ~= nil then
            ver,ref = strReturn:match('(%d.*),(%d*)')
        if strReturn:sub(1,5) == 'Error' then
           msg = strReturn:sub(7)
        end
        end
        print(msg)
        return ver,ref,msg
        
    else
        return nil
    end
end

function cvtVer(s)
if s:lower() == 'fh' then
   s = table.concat({fhGetAppVersion()},'.')
end
local ver,factor,max = 0,10000,3
local t = {}
for word in s:gmatch("%d+") do
 table.insert(t,tonumber(word)) 
end
for j = 1,max do
t[j] = t[j] or 0
ver = ver + (t[j] * factor^(max-j))
end
return ver
end



function dirtree(dir)
    assert(dir and dir ~= "", "directory parameter is missing or empty")
    if string.sub(dir, -1) == "\\" then
        dir=string.sub(dir, 1, -2)
    end
    
    local function yieldtree(dir)
        for entry in lfs.dir(dir) do
            if entry ~= "." and entry ~= ".." then
                entry=dir.."\\"..entry
                local attr=lfs.attributes(entry)
                coroutine.yield(entry,attr)
                if attr ~= nil and attr.mode == "directory" then
                    yieldtree(entry)
                    print(entry)
                end
            end
        end
    end
    return coroutine.wrap(function() yieldtree(dir) end)
end
function splitfilename(strfilename)
    -- Returns the Path Filename and extension as 3 values
    return string.match(strfilename, "(.-)([^\\]-([^%.]+))$")
end
-- Open File and return Handle --
function OpenFile(strFileName,strMode)
    local fileHandle, strError = io.open(strFileName,strMode)
    if not fileHandle then
        error("\n Unable to open file in \""..strMode.."\" mode. \n "..strFileName.." \n "..tostring(strError).." \n")
    end
    return fileHandle
end -- OpenFile
-- Load string from file --
function StrLoadFromFile(strFileName)
    local fileHandle = OpenFile(strFileName,"r")
    local strString = fileHandle:read("*all")
    assert(fileHandle:close())
    return strString
end
-- StrLoadFromFile
function SaveStringToFile(strString,strFileName)
    local fileHandle = OpenFile(strFileName,"w")
    fileHandle:write(strString)
    assert(fileHandle:close())
end -- SaveStringToFile
-- Progress Bar
ProgressBar = {
    Start = function(strTitle,intMax) -- Create & start Progress Bar window
        if not DlgGauge then
            IsBarStop = false
            IntStart = os.time()
            IntDelta = 0
            StrClock = "00:00:00"
            local btnStop = iup.button { title="Stop "..strTitle, rastersize="200x30", action=function() IsBarStop = true end, } -- Signal Stop button pressed return iup.CLOSE -- Often caused main GUI to close !!!
             BarGauge = iup.progressbar { rastersize="400x30", value=0, max=intMax, } -- Progress bar maximum range
             LblText = iup.label { title=" ", expand="YES", alignment="ACENTER", tip="Percentage and Time Elapsed", }
            DlgGauge = iup.dialog { title=strTitle.." Progress", dialogframe="YES", -- Remove Windows minimize/maximize menu
                iup.vbox{ alignment="ACENTER", gap="10",
                LblText,
                BarGauge,
                btnStop,
            },
            move_cb = function(self,x,y) IntDataX=x IntDataY=y end,
            close_cb = btnStop.action, -- Windows Close button = Stop button
        }
        if sFHVer >= 7 then
				iup.SetAttribute(DlgGauge, "NATIVEPARENT", fhGetContextInfo("CI_PARENT_HWND"));
		  end	  
        DlgGauge:showxy(IntDataX,IntDataY) -- Show the Progress Bar window
        sHandle = DlgGauge.HWND
    end
end,
SetText = function(strText) -- Show the Progress text message
    if DlgGauge then LblText.title = strText end
end,
Step = function(intStep) -- Step the Progress Bar forward
    if DlgGauge then
        local intVal = tonumber(BarGauge.value)
        local intMax = tonumber(BarGauge.max)
        intVal = intVal + intStep
        if intVal > intMax then intVal = intMax end -- Ensure value does not exceed maximum
        BarGauge.value = intVal
        local intDelta = os.difftime(os.time(),IntStart)
        if IntDelta < intDelta then -- Update clock of elapsed time
            IntDelta = intDelta
            local intHour = math.floor( intDelta / 3600 )
            local intMins = math.floor( intDelta / 60 - intHour * 60 )
            local intSecs = intDelta - intMins * 60 - intHour * 3600
            StrClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
        end
        LblText.title = string.format("%4d %% %s ", math.floor( intVal / intMax * 100 ), StrClock) -- Display % and clock progress
        DlgGauge.bringfront = "YES"
        iup.LoopStep()
    end
end,
Reset = function() -- Reset the Progress Bar
    if DlgGauge then BarGauge.value = 0 end
end,
Stop = function() -- Check if Stop button pressed
    return IsBarStop
end,
Close = function() -- Close the Progress Bar window
    IsBarStop = false
    if DlgGauge then DlgGauge:destroy() DlgGauge = nil end
end,
} -- ProgressBar
function sumtbl(tbl)
local tot = 0
for _,v in ipairs(tbl) do
    tot = tot + (tonumber(v) or 0)
end
return tot
end

function stripz(s)
  return (s:gsub( "%z", "" ))
end
-- Execute Main Function
main()

Source:Check-Installed-Plugins-Against-the-Store-5.fh_lua