--[[ @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()