# Banimark server setup for Windows - checks this server can run Banimark and # installs the ionCube Loader if it is missing. XAMPP, WampServer, Laragon, # Herd, IIS and plain PHP installs. # # irm https://banimark.com/setup.ps1 | iex # # With options (report only / a specific php.exe): # & ([scriptblock]::Create((irm https://banimark.com/setup.ps1))) -Check # & ([scriptblock]::Create((irm https://banimark.com/setup.ps1))) -Php 'C:\xampp\php\php.exe' # # Written for Windows PowerShell 5.1 (what every Windows ships), so no ternary, # no ?? and no && . Every change is announced, asked about, backed up, checked # afterwards - and undone if PHP does not start with it. param( [switch]$Check, [switch]$Yes, [string]$Php = '' ) # `irm | iex` runs this INSIDE the user's own session: `exit` would close their # window and a changed $ErrorActionPreference would outlive the script. So: # `return` to stop, and -ErrorAction Stop only on the calls that need it. $HqUrl = 'https://banimark.com' $MinPhp = [version]'8.2' $MinLoader = 15 $LoaderBase = 'https://downloads.ioncube.com/loader_downloads' $LoadersPage = 'https://www.ioncube.com/loaders.php' $Required = @('json', 'curl', 'pdo', 'mbstring', 'sodium') $Recommended = @('zip') [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 function Head($t) { Write-Host ''; Write-Host $t -ForegroundColor Magenta } function Ok($t) { Write-Host " [ok] $t" -ForegroundColor Green } function Bad($t) { Write-Host " [x] $t" -ForegroundColor Red } function Warn($t) { Write-Host " [!] $t" -ForegroundColor Yellow } function Note($t) { Write-Host " $t" -ForegroundColor DarkGray } function Ask($q, $default = 'y') { if ($Check) { return $false } if ($Yes) { return $true } $hint = '[Y/n]'; if ($default -ne 'y') { $hint = '[y/N]' } $r = Read-Host " ? $q $hint" if ($r -eq '') { return ($default -eq 'y') } return ($r -match '^[Yy]') } function AskLine($q, $default = '') { if ($Yes) { return $default } $shown = $q; if ($default -ne '') { $shown = "$q [$default]" } $r = Read-Host " ? $shown" if ($r -eq '') { return $default } return $r } # no BOM: PHP's ini parser reads a BOM as text on the first line $NoBom = New-Object System.Text.UTF8Encoding $false Write-Host 'Banimark server setup' -ForegroundColor White if ($Check) { Note 'Check only - nothing on this server will be changed.' } $IsAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) $Arch = 'x86-64'; if (-not [Environment]::Is64BitOperatingSystem) { $Arch = 'x86' } Head 'This server' Ok ("Windows " + [Environment]::OSVersion.Version + ", " + $env:PROCESSOR_ARCHITECTURE) if ($IsAdmin) { Ok 'Running as Administrator' } else { Warn 'Not running as Administrator - PHP under Program Files cannot be changed. Right-click PowerShell -> Run as administrator if a step fails.' } # ------------------------------------------------------------------ which PHP function Find-Php { $list = New-Object System.Collections.Generic.List[string] Get-Command php -All -ErrorAction SilentlyContinue | ForEach-Object { $list.Add($_.Source) } $globs = @( 'C:\php\php.exe', 'C:\php*\php.exe', 'C:\xampp\php\php.exe', 'C:\laragon\bin\php\*\php.exe', 'C:\wamp64\bin\php\*\php.exe', 'C:\wamp\bin\php\*\php.exe', 'C:\Program Files\PHP\*\php.exe', 'C:\Program Files\PHP\php.exe', 'C:\tools\php*\php.exe', "$env:USERPROFILE\.config\herd\bin\php*\php.exe", "$env:USERPROFILE\scoop\apps\php*\current\php.exe" ) foreach ($g in $globs) { Get-Item $g -ErrorAction SilentlyContinue | ForEach-Object { $list.Add($_.FullName) } } $seen = @{} foreach ($p in $list) { $k = $p.ToLower() if (-not $seen.ContainsKey($k) -and (Test-Path $p)) { $seen[$k] = $true; $p } } } if ($Php -eq '') { $found = @(Find-Php) if ($found.Count -eq 0) { Head 'PHP' Bad 'No php.exe found.' Note 'Install PHP 8.2 or newer (https://windows.php.net/download - the Non Thread Safe x64 zip for IIS/FastCGI,' Note 'Thread Safe for Apache), or a bundle such as XAMPP or Laragon. Then run this again.' return } elseif ($found.Count -eq 1) { $Php = $found[0] } else { Head 'PHP versions found' for ($i = 0; $i -lt $found.Count; $i++) { $v = (& $found[$i] -r 'echo PHP_VERSION;' 2>$null) Write-Host (" {0}) PHP {1,-8} {2}" -f ($i + 1), $v, $found[$i]) } Note 'Pick the PHP your WEBSITE runs on (XAMPP/Laragon/WampServer menu, or IIS -> Handler Mappings).' $pick = AskLine 'Which one? (number)' '1' $n = 0; if (-not [int]::TryParse($pick, [ref]$n) -or $n -lt 1 -or $n -gt $found.Count) { $n = 1 } $Php = $found[$n - 1] } } if (-not (Test-Path $Php)) { Bad "That PHP ($Php) does not exist."; return } $PhpDir = Split-Path -Parent (Resolve-Path $Php).Path $probe = @' echo "ver=", PHP_VERSION, "\n"; echo "mm=", PHP_MAJOR_VERSION, ".", PHP_MINOR_VERSION, "\n"; echo "zts=", PHP_ZTS, "\n"; echo "bits=", PHP_INT_SIZE * 8, "\n"; echo "extdir=", ini_get("extension_dir"), "\n"; echo "ini=", (string) php_ini_loaded_file(), "\n"; echo "exts=", strtolower(implode(",", get_loaded_extensions())), ",\n"; echo "pdo=", class_exists("PDO") ? implode(",", PDO::getAvailableDrivers()) : "", "\n"; echo "ioncube=", function_exists("ioncube_loader_version") ? ioncube_loader_version() : "", "\n"; '@ function Probe { $info = @{} # -r with a here-string: PowerShell 5.1 mangles embedded double quotes on # native command lines, so the code goes through a temp file instead $f = Join-Path $env:TEMP ("bm-probe-" + [guid]::NewGuid().ToString('N') + '.php') [IO.File]::WriteAllText($f, "$null } finally { Remove-Item $f -ErrorAction SilentlyContinue } foreach ($line in $out) { $i = $line.IndexOf('='); if ($i -gt 0) { $info[$line.Substring(0, $i)] = $line.Substring($i + 1) } } return $info } $I = Probe if (-not $I.ContainsKey('ver')) { Bad "$Php would not run. Its own error:"; & $Php -v 2>&1 | Select-Object -First 3; return } # "PHP Extension Build => API20230831,NTS,VS16": which compiler the loader must match $build = (& $Php -i 2>$null | Select-String 'PHP Extension Build' | Select-Object -First 1) $Vc = 'vc16' if ($build -and $build.ToString() -match 'VS(\d+)') { $Vc = 'vc' + $Matches[1] } elseif ([version]$I['mm'] -ge [version]'8.4') { $Vc = 'vc17' } $Ts = ($I['zts'] -eq '1') $ExtDir = $I['extdir'] if ($ExtDir -ne '' -and -not [IO.Path]::IsPathRooted($ExtDir)) { $ExtDir = Join-Path $PhpDir $ExtDir } if ($ExtDir -eq '') { $ExtDir = Join-Path $PhpDir 'ext' } $Ini = $I['ini'] # ------------------------------------------------------------------ requirements $Failed = $false $NeedLoader = $false $tsLabel = 'NTS'; if ($Ts) { $tsLabel = 'TS' } Head ("PHP " + $I['ver'] + " ($tsLabel, $Vc, " + $I['bits'] + "-bit) " + $Php) if ([version]$I['mm'] -ge $MinPhp) { Ok ("PHP " + $I['mm'] + " (Banimark needs $MinPhp or newer)") } else { Bad ("PHP " + $I['mm'] + " is too old - Banimark needs $MinPhp or newer"); $Failed = $true } $exts = ',' + $I['exts'] $Missing = @() foreach ($e in $Required) { if ($exts.Contains(",$e,")) { Ok "ext-$e" } else { Bad "ext-$e is missing (required)"; $Missing += $e; $Failed = $true } } $drivers = ',' + $I['pdo'] + ',' if ($drivers -match ',(mysql|pgsql|sqlite),') { Ok ("a database driver (" + $I['pdo'] + ")") } else { Bad 'no database driver for PDO (needs pdo_mysql, pdo_pgsql or pdo_sqlite)'; $Missing += 'pdo_mysql'; $Failed = $true } foreach ($e in $Recommended) { if ($exts.Contains(",$e,")) { Ok "ext-$e" } else { Warn "ext-$e is missing - one-click updates from the admin need it"; $Missing += $e } } $lv = $I['ioncube'] if ($lv -ne '') { if ([int]($lv.Split('.')[0]) -ge $MinLoader) { Ok "ionCube Loader $lv" } else { Bad "ionCube Loader $lv is too old - Banimark needs $MinLoader or newer"; $NeedLoader = $true } } else { Bad "ionCube Loader is not installed (Banimark's core needs it)"; $NeedLoader = $true } if (Get-Command composer -ErrorAction SilentlyContinue) { Ok 'Composer' } else { Warn 'Composer is not installed (only needed if you install Banimark with Composer): https://getcomposer.org/Composer-Setup.exe' } try { Invoke-WebRequest -UseBasicParsing -TimeoutSec 10 "$HqUrl/api/v1/releases" -ErrorAction Stop | Out-Null; Ok 'Can reach Banimark HQ (licence checks and updates)' } catch { Warn "Could not reach $HqUrl - licence checks and updates need outbound HTTPS to it" } # ------------------------------------------------------------------ extensions shipped but switched off # Windows PHP ships every common extension as a DLL in ext\; "missing" nearly # always means a commented-out line in php.ini. if ($Missing.Count -gt 0 -and $Ini -ne '') { $lines = [IO.File]::ReadAllLines($Ini) $fixable = @() foreach ($e in $Missing) { if (Test-Path (Join-Path $ExtDir "php_$e.dll")) { for ($n = 0; $n -lt $lines.Length; $n++) { if ($lines[$n] -match "^\s*;\s*extension\s*=\s*(php_)?$e(\.dll)?\s*$") { $fixable += $e; break } } } } if ($fixable.Count -gt 0) { Head ("Switched off in php.ini: " + ($fixable -join ', ')) if (Ask 'Switch them on?' 'y') { Copy-Item $Ini "$Ini.bak-banimark-ext" -Force for ($n = 0; $n -lt $lines.Length; $n++) { foreach ($e in $fixable) { if ($lines[$n] -match "^\s*;\s*extension\s*=\s*(php_)?$e(\.dll)?\s*$") { $lines[$n] = "extension=$e" } } } [IO.File]::WriteAllLines($Ini, $lines, $NoBom) Ok "Switched on (backup: $Ini.bak-banimark-ext)" $I = Probe; $exts = ',' + $I['exts'] $Failed = $false foreach ($e in $Required) { if (-not $exts.Contains(",$e,")) { $Failed = $true } } } } } # ------------------------------------------------------------------ ionCube Loader $Done = $false if ($NeedLoader) { Head ("Installing the ionCube Loader for PHP " + $I['mm']) if ($Check) { Note 'Run again without -Check to install it.' } elseif (Ask 'Download the ionCube Loader and install it?' 'y') { if ($Ini -eq '') { $tpl = Join-Path $PhpDir 'php.ini-production' $Ini = Join-Path $PhpDir 'php.ini' if (Test-Path $tpl) { Copy-Item $tpl $Ini; Ok "Created $Ini from php.ini-production" } else { [IO.File]::WriteAllText($Ini, '', $NoBom); Ok "Created an empty $Ini" } } $arch = 'x86-64'; if ($I['bits'] -eq '32') { $arch = 'x86' } $zipName = 'ioncube_loaders_win_' if (-not $Ts) { $zipName += 'nonts_' } $zipName += "${Vc}_$arch.zip" $tmp = Join-Path $env:TEMP ('bm-ioncube-' + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory $tmp | Out-Null try { Note "Downloading $LoaderBase/$zipName ..." Invoke-WebRequest -UseBasicParsing "$LoaderBase/$zipName" -OutFile "$tmp\l.zip" -ErrorAction Stop Expand-Archive "$tmp\l.zip" -DestinationPath "$tmp\x" -Force -ErrorAction Stop $dll = Get-ChildItem "$tmp\x" -Recurse -Filter ("ioncube_loader_win_" + $I['mm'] + ".dll") | Select-Object -First 1 if (-not $dll) { Bad ("The archive has no loader for PHP " + $I['mm'] + " yet - see $LoadersPage"); return } $dest = Join-Path $ExtDir $dll.Name Copy-Item $dll.FullName $dest -Force -ErrorAction Stop Ok "Loader copied to $dest" $backup = "$Ini.bak-banimark-" + (Get-Date -Format 'yyyyMMddHHmmss') Copy-Item $Ini $backup -ErrorAction Stop $old = [IO.File]::ReadAllLines($Ini) $new = New-Object System.Collections.Generic.List[string] $new.Add('; Added by Banimark setup. The ionCube Loader must be the FIRST zend_extension PHP loads.') $new.Add("zend_extension=`"$dest`"") $new.Add('') foreach ($l in $old) { if ($l -match '^\s*zend_extension\s*=.*ioncube') { $new.Add("; $l ; replaced by Banimark setup") } else { $new.Add($l) } } [IO.File]::WriteAllLines($Ini, $new.ToArray(), $NoBom) Ok "Loader added to the top of $Ini" Note "Backup: $backup" $v = (& $Php -v 2>&1 | Out-String) if ($v -match 'ionCube PHP Loader') { Ok ("PHP now starts with: " + ([regex]::Match($v, 'ionCube PHP Loader[^,]*').Value)) $Done = $true; $NeedLoader = $false } else { Bad 'PHP does not start cleanly with the loader:' ($v -split "`n" | Select-Object -First 4) | ForEach-Object { Note $_ } Copy-Item $backup $Ini -Force Remove-Item $dest -ErrorAction SilentlyContinue Warn "Undone - $Ini is back as it was." } } catch { Bad ("Could not install it: " + $_.Exception.Message) Note "Download it yourself from $LoadersPage ($zipName)." } finally { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue } } } if ($Done) { Head 'Restart PHP so the website picks it up' $svc = @(Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq 'Running' -and ($_.Name -match '^(W3SVC|Apache.*|wampapache.*|php.*)$') }) if ($svc.Count -gt 0) { Note ('Running here: ' + (($svc | ForEach-Object { $_.Name }) -join ', ')) if ($IsAdmin -and (Ask 'Restart them now?' 'y')) { foreach ($s in $svc) { try { Restart-Service $s.Name -Force; Ok ("Restarted " + $s.Name) } catch { Warn ("Could not restart " + $s.Name) } } } } Note 'XAMPP: Stop and Start Apache in the Control Panel. Laragon: Reload. WampServer: Restart All Services.' Note 'IIS: iisreset. php artisan serve / Herd: restart it.' } # ------------------------------------------------------------------ the website's PHP if (-not $Check -and -not $Yes) { Head "Check the website's PHP too" Note "The terminal's PHP and the website's are often not the same. This puts a tiny," Note 'randomly named check file in the site''s public folder, reads it once, and deletes it.' if (Ask 'Check the website now?' 'y') { $root = AskLine "The site's public folder (where index.php is)" (Get-Location).Path if ((Test-Path "$root\public") -and -not (Test-Path "$root\index.php")) { $root = "$root\public" } if (Test-Path $root) { $name = 'bm-check-' + [guid]::NewGuid().ToString('N').Substring(0, 12) + '.php' $file = Join-Path $root $name $code = '