From 2ce8351b1aeb59037de6279576c55dc9d3983ca8 Mon Sep 17 00:00:00 2001 From: NL-TCH Date: Thu, 8 Feb 2024 23:24:30 +0100 Subject: [PATCH 01/51] initial install test --- api/auth.py | 20 ++++ api/main.py | 229 +++++++++++++++++++++++++++++++++++++ api/modules/ap.py | 112 ++++++++++++++++++ api/modules/client.py | 28 +++++ api/modules/ddns.py | 24 ++++ api/modules/dhcp.py | 30 +++++ api/modules/dns.py | 38 ++++++ api/modules/firewall.py | 4 + api/modules/networking.py | 68 +++++++++++ api/modules/openvpn.py | 41 +++++++ api/modules/restart.py | 7 ++ api/modules/system.py | 86 ++++++++++++++ api/modules/wireguard.py | 26 +++++ api/requirements.txt | 3 + installers/common.sh | 32 ++++++ installers/raspbian.sh | 5 + installers/restapi.service | 14 +++ 17 files changed, 767 insertions(+) create mode 100644 api/auth.py create mode 100644 api/main.py create mode 100644 api/modules/ap.py create mode 100644 api/modules/client.py create mode 100644 api/modules/ddns.py create mode 100644 api/modules/dhcp.py create mode 100644 api/modules/dns.py create mode 100644 api/modules/firewall.py create mode 100644 api/modules/networking.py create mode 100644 api/modules/openvpn.py create mode 100644 api/modules/restart.py create mode 100644 api/modules/system.py create mode 100644 api/modules/wireguard.py create mode 100644 api/requirements.txt create mode 100644 installers/restapi.service diff --git a/api/auth.py b/api/auth.py new file mode 100644 index 00000000..2a716e2d --- /dev/null +++ b/api/auth.py @@ -0,0 +1,20 @@ +import os +from fastapi.security.api_key import APIKeyHeader +from fastapi import Security, HTTPException +from starlette.status import HTTP_403_FORBIDDEN + +apikey=os.getenv('RASPAP_API_KEY') +#if env not set, set the api key to "insecure" +if apikey == None: + apikey = "insecure" + +print(apikey) +api_key_header = APIKeyHeader(name="access_token", auto_error=False) + +async def get_api_key(api_key_header: str = Security(api_key_header)): + if api_key_header ==apikey: + return api_key_header + else: + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="403: Unauthorized" + ) \ No newline at end of file diff --git a/api/main.py b/api/main.py new file mode 100644 index 00000000..a3153f0c --- /dev/null +++ b/api/main.py @@ -0,0 +1,229 @@ +from fastapi import FastAPI, Depends +from fastapi.security.api_key import APIKey +import auth + +import json + +import modules.system as system +import modules.ap as ap +import modules.client as client +import modules.dns as dns +import modules.dhcp as dhcp +import modules.ddns as ddns +import modules.firewall as firewall +import modules.networking as networking +import modules.openvpn as openvpn +import modules.wireguard as wireguard +import modules.restart as restart + + +tags_metadata = [ + { + "name": "system", + "description": "All information regarding the underlying system." + }, + { + "name": "accesspoint/hostpost", + "description": "Get and change all information regarding the hotspot" + } +] +app = FastAPI( + title="API for Raspap", + openapi_tags=tags_metadata, + version="0.0.1", + license_info={ + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + } +) + +@app.get("/system", tags=["system"]) +async def get_system(): + return{ +'hostname': system.hostname(), +'uptime': system.uptime(), +'systime': system.systime(), +'usedMemory': system.usedMemory(), +'processorCount': system.processorCount(), +'LoadAvg1Min': system.LoadAvg1Min(), +'systemLoadPercentage': system.systemLoadPercentage(), +'systemTemperature': system.systemTemperature(), +'hostapdStatus': system.hostapdStatus(), +'operatingSystem': system.operatingSystem(), +'kernelVersion': system.kernelVersion(), +'rpiRevision': system.rpiRevision() +} + +@app.get("/ap", tags=["accesspoint/hostpost"]) +async def get_ap(): + return{ +'driver': ap.driver(), +'ctrl_interface': ap.ctrl_interface(), +'ctrl_interface_group': ap.ctrl_interface_group(), +'auth_algs': ap.auth_algs(), +'wpa_key_mgmt': ap.wpa_key_mgmt(), +'beacon_int': ap.beacon_int(), +'ssid': ap.ssid(), +'channel': ap.channel(), +'hw_mode': ap.hw_mode(), +'ieee80211n': ap.ieee80211n(), +'wpa_passphrase': ap.wpa_passphrase(), +'interface': ap.interface(), +'wpa': ap.wpa(), +'wpa_pairwise': ap.wpa_pairwise(), +'country_code': ap.country_code(), +'ignore_broadcast_ssid': ap.ignore_broadcast_ssid() +} + +@app.post("/ap", tags=["accesspoint/hostpost"]) +async def post_ap(driver=None, + ctrl_interface=None, + ctrl_interface_group=None, + auth_algs=None, + wpa_key_mgmt=None, + beacon_int=None, + ssid=None, + channel=None, + hw_mode=None, + ieee80211n=None, + wpa_passphrase=None, + interface=None, + wpa=None, + wpa_pairwise=None, + country_code=None, + ignore_broadcast_ssid=None, + api_key: APIKey = Depends(auth.get_api_key)): + if driver != None: + ap.set_driver(driver) + if ctrl_interface != None: + ap.set_ctrl_interface(ctrl_interface) + if ctrl_interface_group !=None: + ap.set_ctrl_interface_group(ctrl_interface_group) + if auth_algs != None: + ap.set_auth_algs(auth_algs) + if wpa_key_mgmt != None: + ap.set_wpa_key_mgmt(wpa_key_mgmt) + if beacon_int != None: + ap.set_beacon_int(beacon_int) + if ssid != None: + ap.set_ssid(ssid) + if channel != None: + ap.set_channel(channel) + if hw_mode != None: + ap.set_hw_mode(hw_mode) + if ieee80211n != None: + ap.set_ieee80211n(ieee80211n) + if wpa_passphrase != None: + ap.set_wpa_passphrase(wpa_passphrase) + if interface != None: + ap.set_interface(interface) + if wpa != None: + ap.set_wpa(wpa) + if wpa_pairwise != None: + ap.set_wpa_pairwise(wpa_pairwise) + if country_code != None: + ap.set_country_code(country_code) + if ignore_broadcast_ssid != None: + ap.set_ignore_broadcast_ssid(ignore_broadcast_ssid) + + +@app.get("/clients/{wireless_interface}", tags=["Clients"]) +async def get_clients(wireless_interface): + return{ +'active_clients_amount': client.get_active_clients_amount(wireless_interface), +'active_clients': json.loads(client.get_active_clients(wireless_interface)) +} + +@app.get("/dhcp", tags=["DHCP"]) +async def get_dhcp(): + return{ +'range_start': dhcp.range_start(), +'range_end': dhcp.range_end(), +'range_subnet_mask': dhcp.range_subnet_mask(), +'range_lease_time': dhcp.range_lease_time(), +'range_gateway': dhcp.range_gateway(), +'range_nameservers': dhcp.range_nameservers() +} + +@app.get("/dns/domains", tags=["DNS"]) +async def get_domains(): + return{ +'domains': json.loads(dns.adblockdomains()) +} +@app.get("/dns/hostnames", tags=["DNS"]) +async def get_hostnames(): + return{ +'hostnames': json.loads(dns.adblockhostnames()) +} + +@app.get("/dns/upstream", tags=["DNS"]) +async def get_upstream(): + return{ +'upstream_nameserver': dns.upstream_nameserver() +} + +@app.get("/dns/logs", tags=["DNS"]) +async def get_dnsmasq_logs(): + return(dns.dnsmasq_logs()) + + +@app.get("/ddns", tags=["DDNS"]) +async def get_ddns(): + return{ +'use': ddns.use(), +'method': ddns.method(), +'protocol': ddns.protocol(), +'server': ddns.server(), +'login': ddns.login(), +'password': ddns.password(), +'domain': ddns.domain() +} + +@app.get("/firewall", tags=["Firewall"]) +async def get_firewall(): + return json.loads(firewall.firewall_rules()) + +@app.get("/networking", tags=["Networking"]) +async def get_networking(): + return{ +'interfaces': json.loads(networking.interfaces()), +'throughput': json.loads(networking.throughput()) +} + +@app.get("/openvpn", tags=["OpenVPN"]) +async def get_openvpn(): + return{ +'client_configs': openvpn.client_configs(), +'client_config_names': openvpn.client_config_names(), +'client_config_active': openvpn.client_config_active(), +'client_login_names': openvpn.client_login_names(), +'client_login_active': openvpn.client_login_active() +} + +@app.get("/openvpn/{config}", tags=["OpenVPN"]) +async def client_config_list(config): + return{ +'client_config': openvpn.client_config_list(config) +} + +@app.get("/wireguard", tags=["WireGuard"]) +async def get_wireguard(): + return{ +'client_configs': wireguard.configs(), +'client_config_names': wireguard.client_config_names(), +'client_config_active': wireguard.client_config_active() +} + +@app.get("/wireguard/{config}", tags=["WireGuard"]) +async def client_config_list(config): + return{ +'client_config': wireguard.client_config_list(config) +} + +@app.post("/restart/webgui") +async def restart_webgui(): + restart.webgui() + +@app.post("/restart/adblock") +async def restart_adblock(): + restart.adblock() \ No newline at end of file diff --git a/api/modules/ap.py b/api/modules/ap.py new file mode 100644 index 00000000..2d6fd8e3 --- /dev/null +++ b/api/modules/ap.py @@ -0,0 +1,112 @@ +import subprocess +import json + +def driver(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep driver= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_driver(driver): + return subprocess.run(f"sudo sed -i 's/^driver=.*/driver={driver}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def ctrl_interface(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ctrl_interface= | cut -d'=' -f2 | head -1", shell=True, capture_output=True, text=True).stdout.strip() + +def set_ctrl_interface(ctrl_interface): + return subprocess.run(f"sudo sed -i 's/^ctrl_interface=.*/ctrl_interface={ctrl_interface}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def ctrl_interface_group(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ctrl_interface_group= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_ctrl_interface_group(ctrl_interface_group): + return subprocess.run(f"sudo sed -i 's/^ctrl_interface_group=.*/ctrl_interface_group={ctrl_interface_group}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def auth_algs(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep auth_algs= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_auth_algs(auth_algs): + return subprocess.run(f"sudo sed -i 's/^auth_algs=.*/auth_algs={auth_algs}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def wpa_key_mgmt(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa_key_mgmt= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_wpa_key_mgmt(wpa_key_mgmt): + return subprocess.run(f"sudo sed -i 's/^wpa_key_mgmt=.*/wpa_key_mgmt={wpa_key_mgmt}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def beacon_int(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep beacon_int= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_beacon_int(beacon_int): + return subprocess.run(f"sudo sed -i 's/^beacon_int=.*/beacon_int={beacon_int}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def ssid(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ssid= | cut -d'=' -f2 | head -1", shell=True, capture_output=True, text=True).stdout.strip() + +def set_ssid(ssid): + return subprocess.run(f"sudo sed -i 's/^ssid=.*/ssid={ssid}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def channel(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep channel= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_channel(channel): + return subprocess.run(f"sudo sed -i 's/^channel=.*/channel={channel}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def hw_mode(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep hw_mode= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_hw_mode(hw_mode): + return subprocess.run(f"sudo sed -i 's/^hw_mode=.*/hw_mode={hw_mode}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def ieee80211n(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ieee80211n= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_ieee80211n(ieee80211n): + return subprocess.run(f"sudo sed -i 's/^ieee80211n=.*/ieee80211n={ieee80211n}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def wpa_passphrase(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa_passphrase= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_wpa_passphrase(wpa_passphrase): + return subprocess.run(f"sudo sed -i 's/^wpa_passphrase=.*/wpa_passphrase={wpa_passphrase}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def interface(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep interface= | cut -d'=' -f2 | head -1", shell=True, capture_output=True, text=True).stdout.strip() + +def set_interface(interface): + return subprocess.run(f"sudo sed -i 's/^interface=.*/interface={interface}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def wpa(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_wpa(wpa): + return subprocess.run(f"sudo sed -i 's/^wpa=.*/wpa={wpa}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def wpa_pairwise(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa_pairwise= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_wpa_pairwise(wpa_pairwise): + return subprocess.run(f"sudo sed -i 's/^wpa_pairwise=.*/wpa_pairwise={wpa_pairwise}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def country_code(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep country_code= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_country_code(country_code): + return subprocess.run(f"sudo sed -i 's/^country_code=.*/country_code={country_code}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def ignore_broadcast_ssid(): + return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ignore_broadcast_ssid= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def set_ignore_broadcast_ssid(ignore_broadcast_ssid): + return subprocess.run(f"sudo sed -i 's/^ignore_broadcast_ssid=.*/ignore_broadcast_ssid={ignore_broadcast_ssid}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def logging(): + log_output = subprocess.run(f"cat /tmp/hostapd.log", shell=True, capture_output=True, text=True).stdout.strip() + logs = {} + + for line in log_output.split('\n'): + parts = line.split(': ') + if len(parts) >= 2: + interface, message = parts[0], parts[1] + if interface not in logs: + logs[interface] = [] + logs[interface].append(message) + + return json.dumps(logs, indent=2) \ No newline at end of file diff --git a/api/modules/client.py b/api/modules/client.py new file mode 100644 index 00000000..2e894cd5 --- /dev/null +++ b/api/modules/client.py @@ -0,0 +1,28 @@ +import subprocess +import json + +def get_active_clients_amount(interface): + output = subprocess.run(f'''cat '/var/lib/misc/dnsmasq.leases' | grep -iwE "$(arp -i '{interface}' | grep -oE "(([0-9]|[a-f]|[A-F]){{{2}}}:){{{5}}}([0-9]|[a-f]|[A-F]){{{2}}}")"''', shell=True, capture_output=True, text=True) + return(len(output.stdout.splitlines())) + +def get_active_clients(interface): + #does not run like intended, but it works.... + output = subprocess.run(f'''cat '/var/lib/misc/dnsmasq.leases' | grep -iwE "$(arp -i '{interface}' | grep -oE "(([0-9]|[a-f]|[A-F]){{{2}}}:){{{5}}}([0-9]|[a-f]|[A-F]){{{2}}}")"''', shell=True, capture_output=True, text=True) + clients_list = [] + + for line in output.stdout.splitlines(): + fields = line.split() + + client_data = { + "timestamp": int(fields[0]), + "mac_address": fields[1], + "ip_address": fields[2], + "hostname": fields[3], + "client_id": fields[4], + } + + clients_list.append(client_data) + + json_output = json.dumps(clients_list, indent=2) + + return json_output \ No newline at end of file diff --git a/api/modules/ddns.py b/api/modules/ddns.py new file mode 100644 index 00000000..e7ab3ebf --- /dev/null +++ b/api/modules/ddns.py @@ -0,0 +1,24 @@ +import subprocess + +def use(): + return subprocess.run("cat /etc/ddclient.conf | grep use= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def method(): + #get the contents of the line below "use=" + return subprocess.run("awk '/^use=/ {getline; print}' /etc/ddclient.conf | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def protocol(): + return subprocess.run("cat /etc/ddclient.conf | grep protocol= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def server(): + return subprocess.run("cat /etc/ddclient.conf | grep server= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def login(): + return subprocess.run("cat /etc/ddclient.conf | grep login= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def password(): + return subprocess.run("cat /etc/ddclient.conf | grep password= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def domain(): + #get the contents of the line below "password=" + return subprocess.run("awk '/^password=/ {getline; print}' /etc/ddclient.conf", shell=True, capture_output=True, text=True).stdout.strip() \ No newline at end of file diff --git a/api/modules/dhcp.py b/api/modules/dhcp.py new file mode 100644 index 00000000..887bb29b --- /dev/null +++ b/api/modules/dhcp.py @@ -0,0 +1,30 @@ +import subprocess +import json + +def range_start(): + return subprocess.run("cat /etc/dnsmasq.d/090_wlan0.conf |grep dhcp-range= |cut -d'=' -f2| cut -d',' -f1", shell=True, capture_output=True, text=True).stdout.strip() + +def range_end(): + return subprocess.run("cat /etc/dnsmasq.d/090_wlan0.conf |grep dhcp-range= |cut -d'=' -f2| cut -d',' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def range_subnet_mask(): + return subprocess.run("cat /etc/dnsmasq.d/090_wlan0.conf |grep dhcp-range= |cut -d'=' -f2| cut -d',' -f3", shell=True, capture_output=True, text=True).stdout.strip() + +def range_lease_time(): + return subprocess.run("cat /etc/dnsmasq.d/090_wlan0.conf |grep dhcp-range= |cut -d'=' -f2| cut -d',' -f4", shell=True, capture_output=True, text=True).stdout.strip() + +def range_gateway(): + return subprocess.run("cat /etc/dhcpcd.conf | grep routers | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() + +def range_nameservers(): + output = subprocess.run("cat /etc/dhcpcd.conf", shell=True, capture_output=True, text=True).stdout.strip() + + nameservers = [] + + lines = output.split('\n') + for line in lines: + if "static domain_name_server" in line: + servers = line.split('=')[1].strip().split() + nameservers.extend(servers) + + return nameservers \ No newline at end of file diff --git a/api/modules/dns.py b/api/modules/dns.py new file mode 100644 index 00000000..17fb4d1e --- /dev/null +++ b/api/modules/dns.py @@ -0,0 +1,38 @@ +import subprocess +import json + +def adblockdomains(): + output = subprocess.run("cat /etc/raspap/adblock/domains.txt", shell=True, capture_output=True, text=True).stdout.strip() + domains =output.split('\n') + domainlist=[] + for domain in domains: + if domain.startswith('#') or domain=="": + continue + domainlist.append(domain.split('=/')[1]) + return domainlist + +def adblockhostnames(): + output = subprocess.run("cat /etc/raspap/adblock/hostnames.txt", shell=True, capture_output=True, text=True).stdout.strip() + hostnames = output.split('\n') + hostnamelist=[] + for hostname in hostnames: + if hostname.startswith('#') or hostname=="": + continue + hostnamelist.append(hostname.replace('0.0.0.0 ','')) + return hostnamelist + +def upstream_nameserver(): + return subprocess.run("awk '/nameserver/ {print $2}' /run/dnsmasq/resolv.conf", shell=True, capture_output=True, text=True).stdout.strip() + +def dnsmasq_logs(): + output = subprocess.run("cat /var/log/dnsmasq.log", shell=True, capture_output=True, text=True).stdout.strip() + log_entries = [] + for line in output.split("\n"): + fields = line.split(" ") + log_dict = { + 'timestamp': ' '.join(fields[:3]), + 'process': fields[3][:-1], # Remove the trailing colon + 'message': ' '.join(fields[4:]), + } + log_entries.append(log_dict) + return log_entries \ No newline at end of file diff --git a/api/modules/firewall.py b/api/modules/firewall.py new file mode 100644 index 00000000..004b9e4a --- /dev/null +++ b/api/modules/firewall.py @@ -0,0 +1,4 @@ +import subprocess + +def firewall_rules(): + return subprocess.run("cat /etc/raspap/networking/firewall/iptables_rules.json", shell=True, capture_output=True, text=True).stdout.strip() \ No newline at end of file diff --git a/api/modules/networking.py b/api/modules/networking.py new file mode 100644 index 00000000..989e0eb4 --- /dev/null +++ b/api/modules/networking.py @@ -0,0 +1,68 @@ +import psutil +import json + +def throughput(): + interface_info = {} + + # Get network interfaces + interfaces = psutil.net_if_stats() + + for interface, stats in interfaces.items(): + if interface.startswith("lo") or interface.startswith("docker"): + # Skip loopback and docker interface + continue + + try: + # Get network traffic statistics + traffic_stats = psutil.net_io_counters(pernic=True)[interface] + rx_packets = traffic_stats[1] + rx_bytes = traffic_stats[0] + tx_packets = traffic_stats[3] + tx_bytes = traffic_stats[4] + + interface_info[interface] = { + "RX_packets": rx_packets, + "RX_bytes": rx_bytes, + "TX_packets": tx_packets, + "TX_bytes": tx_bytes + } + except KeyError: + # Handle the case where network interface statistics are not available + pass + + return json.dumps(interface_info, indent=2) + +def interfaces(): + interface_info = {} + + # Get network interfaces + interfaces = psutil.net_if_addrs() + + for interface, addrs in interfaces.items(): + if interface.startswith("lo") or interface.startswith("docker"): + # Skip loopback and docker interface + continue + + ip_address = None + netmask = None + mac_address = None + + for addr in addrs: + if addr.family == 2: # AF_INET corresponds to the integer value 2 + # IPv4 address + ip_address = addr.address + netmask = addr.netmask + + # Get MAC address + for addr in psutil.net_if_addrs().get(interface, []): + if addr.family == psutil.AF_LINK: + mac_address = addr.address + + interface_info[interface] = { + "IP_address": ip_address, + "Netmask": netmask, + "MAC_address": mac_address + } + return json.dumps(interface_info, indent=2) + +#TODO: migrate to vnstat, to lose psutil dependency \ No newline at end of file diff --git a/api/modules/openvpn.py b/api/modules/openvpn.py new file mode 100644 index 00000000..b8ad1f96 --- /dev/null +++ b/api/modules/openvpn.py @@ -0,0 +1,41 @@ +import subprocess + +def client_configs(): + return subprocess.run("find /etc/openvpn/client/ -type f | wc -l", shell=True, capture_output=True, text=True).stdout.strip() + +def client_config_names(): + config_names_list = [] + output = subprocess.run('''ls /etc/openvpn/client/ | grep -v "^client.conf$"''', shell=True, capture_output=True, text=True).stdout.strip() + lines = output.split("\n") + for client in lines: + if "_client" in client: + config_names_dict ={'config':client} + config_names_list.append(config_names_dict) + return config_names_list + +def client_login_names(): + config_names_list = [] + output = subprocess.run('''ls /etc/openvpn/client/ | grep -v "^client.conf$"''', shell=True, capture_output=True, text=True).stdout.strip() + lines = output.split("\n") + for client in lines: + if "_login" in client: + config_names_dict ={'login':client} + config_names_list.append(config_names_dict) + return config_names_list + +def client_config_active(): + output = subprocess.run('''ls -al /etc/openvpn/client/ | grep "client.conf -"''', shell=True, capture_output=True, text=True).stdout.strip() + active_config = output.split("/etc/openvpn/client/") + return(active_config[1]) + +def client_login_active(): + output = subprocess.run('''ls -al /etc/openvpn/client/ | grep "login.conf -"''', shell=True, capture_output=True, text=True).stdout.strip() + active_config = output.split("/etc/openvpn/client/") + return(active_config[1]) + +def client_config_list(client_config): + output = subprocess.run(f"cat /etc/openvpn/client/{client_config}", shell=True, capture_output=True, text=True).stdout.strip() + return output.split('\n') + +#TODO: where is the logfile?? +#TODO: is service connected? \ No newline at end of file diff --git a/api/modules/restart.py b/api/modules/restart.py new file mode 100644 index 00000000..18eb1b57 --- /dev/null +++ b/api/modules/restart.py @@ -0,0 +1,7 @@ +import subprocess + +def webgui(): + return subprocess.run("sudo /etc/raspap/lighttpd/configport.sh --restart", shell=True, capture_output=True, text=True).stdout.strip() + +def adblock(): + return subprocess.run("sudo /bin/systemctl restart dnsmasq.service", shell=True, capture_output=True, text=True).stdout.strip() \ No newline at end of file diff --git a/api/modules/system.py b/api/modules/system.py new file mode 100644 index 00000000..82367779 --- /dev/null +++ b/api/modules/system.py @@ -0,0 +1,86 @@ +import subprocess + +revisions = { + '0002': 'Model B Revision 1.0', + '0003': 'Model B Revision 1.0 + ECN0001', + '0004': 'Model B Revision 2.0 (256 MB)', + '0005': 'Model B Revision 2.0 (256 MB)', + '0006': 'Model B Revision 2.0 (256 MB)', + '0007': 'Model A', + '0008': 'Model A', + '0009': 'Model A', + '000d': 'Model B Revision 2.0 (512 MB)', + '000e': 'Model B Revision 2.0 (512 MB)', + '000f': 'Model B Revision 2.0 (512 MB)', + '0010': 'Model B+', + '0013': 'Model B+', + '0011': 'Compute Module', + '0012': 'Model A+', + 'a01041': 'a01041', + 'a21041': 'a21041', + '900092': 'PiZero 1.2', + '900093': 'PiZero 1.3', + '9000c1': 'PiZero W', + 'a02082': 'Pi 3 Model B', + 'a22082': 'Pi 3 Model B', + 'a32082': 'Pi 3 Model B', + 'a52082': 'Pi 3 Model B', + 'a020d3': 'Pi 3 Model B+', + 'a220a0': 'Compute Module 3', + 'a020a0': 'Compute Module 3', + 'a02100': 'Compute Module 3+', + 'a03111': 'Model 4B Revision 1.1 (1 GB)', + 'b03111': 'Model 4B Revision 1.1 (2 GB)', + 'c03111': 'Model 4B Revision 1.1 (4 GB)', + 'c03111': 'Model 4B Revision 1.1 (4 GB)', + 'a03140': 'Compute Module 4 (1 GB)', + 'b03140': 'Compute Module 4 (2 GB)', + 'c03140': 'Compute Module 4 (4 GB)', + 'd03140': 'Compute Module 4 (8 GB)', + 'c04170': 'Pi 5 (4 GB)', + 'd04170': 'Pi 5 (8 GB)' +} + +def hostname(): + return subprocess.run("hostname", shell=True, capture_output=True, text=True).stdout.strip() + +def uptime(): + return subprocess.run("uptime -p", shell=True, capture_output=True, text=True).stdout.strip() + +def systime(): + return subprocess.run("date", shell=True, capture_output=True, text=True).stdout.strip() + +def usedMemory(): + return round(float(subprocess.run("free -m | awk 'NR==2{total=$2 ; used=$3 } END { print used/total*100}'", shell=True, capture_output=True, text=True).stdout.strip()),2) + +def processorCount(): + return int(subprocess.run("nproc --all", shell=True, capture_output=True, text=True).stdout.strip()) + +def LoadAvg1Min(): + return round(float(subprocess.run("awk '{print $1}' /proc/loadavg", shell=True, capture_output=True, text=True).stdout.strip()),2) + +def systemLoadPercentage(): + return round((float(LoadAvg1Min())*100)/float(processorCount()),2) + +def systemTemperature(): + try: + output = subprocess.run("cat /sys/class/thermal/thermal_zone0/temp", shell=True, capture_output=True, text=True).stdout.strip() + return round(float(output)/1000,2) + except ValueError: + return 0 + +def hostapdStatus(): + return int(subprocess.run("pidof hostapd | wc -l", shell=True, capture_output=True, text=True).stdout.strip()) + +def operatingSystem(): + return subprocess.run('''grep PRETTY_NAME /etc/os-release | cut -d= -f2- | sed 's/"//g' ''', shell=True, capture_output=True, text=True).stdout.strip() + +def kernelVersion(): + return subprocess.run("uname -r", shell=True, capture_output=True, text=True).stdout.strip() + +def rpiRevision(): + output = subprocess.run("grep Revision /proc/cpuinfo | awk '{print $3}'", shell=True, capture_output=True, text=True).stdout.strip() + try: + return revisions[output] + except KeyError: + return 'Unknown Device' \ No newline at end of file diff --git a/api/modules/wireguard.py b/api/modules/wireguard.py new file mode 100644 index 00000000..dd33970b --- /dev/null +++ b/api/modules/wireguard.py @@ -0,0 +1,26 @@ +import subprocess + +def configs(): + #ignore symlinks, because wg0.conf is in production the main config, but in insiders it is a symlink + return subprocess.run("find /etc/wireguard/ -type f | wc -l", shell=True, capture_output=True, text=True).stdout.strip() + +def client_config_names(): + config_names_list = [] + output = subprocess.run('''ls /etc/wireguard/ | grep -v "^wg0.conf$"''', shell=True, capture_output=True, text=True).stdout.strip() + lines = output.split("\n") + for client in lines: + config_names_dict ={'config':client} + config_names_list.append(config_names_dict) + return config_names_list + +def client_config_active(): + output = subprocess.run('''ls -al /etc/wireguard/ | grep "wg0.conf -"''', shell=True, capture_output=True, text=True).stdout.strip() + active_config = output.split("/etc/wireguard/") + return(active_config[1]) + +def client_config_list(client_config): + output = subprocess.run(f"cat /etc/wireguard/{client_config}", shell=True, capture_output=True, text=True).stdout.strip() + return output.split('\n') + +#TODO: where is the logfile?? +#TODO: is service connected? \ No newline at end of file diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 00000000..d0b660e0 --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.109.0 +uvicorn==0.25.0 +psutil==5.9.8 \ No newline at end of file diff --git a/installers/common.sh b/installers/common.sh index 8761f941..610d447c 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -57,6 +57,7 @@ function _install_raspap() { _configure_networking _prompt_install_adblock _prompt_install_openvpn + _prompt_isntall_restapi _install_extra_features _prompt_install_wireguard _prompt_install_vpn_providers @@ -502,6 +503,23 @@ function _prompt_install_openvpn() { fi } +# Prompt to install restapi +function _prompt_install_restapi() { + _install_log "Configure restapi" + echo -n "Install and enable RestAPI? [Y/n]: " + if [ "$assume_yes" == 0 ]; then + read answer < /dev/tty + if [ "$answer" != "${answer#[Nn]}" ]; then + _install_status 0 "(Skipped)" + else + _install_restapi + elif [ "$restapi_option" == 1 ]; then + _install_restapi + else + echo "(Skipped)" + fi +} + # Prompt to install WireGuard function _prompt_install_wireguard() { _install_log "Configure WireGuard support" @@ -562,6 +580,20 @@ function _create_openvpn_scripts() { _install_status 0 } +# Install and enable RestAPI configuration option +function _install_restapi() { + _install_log "Installing and enabling RestAPI" + sudo cp -r "$webroot_dir/api" "$raspap_dir/api" || _install_status 1 "Unable to move api folder" + python -m pip install -r "$raspap_dir/api/requirements.txt" || _install_status 1 " Unable to install pip modules" + + echo "Moving restapi systemd unit control file to /lib/systemd/system/" + sudo mv $webroot_dir/installers/restapi.service /lib/systemd/system/ || _install_status 1 "Unable to move restapi.service file" + sudo systemctl daemon-reload + sudo systemctl enable restapi.service || _install_status 1 "Failed to enable restapi.service" + + _install_status 0 +} + # Fetches latest files from github to webroot function _download_latest_files() { _install_log "Cloning latest files from GitHub" diff --git a/installers/raspbian.sh b/installers/raspbian.sh index 6217f57e..ba43c04d 100755 --- a/installers/raspbian.sh +++ b/installers/raspbian.sh @@ -94,6 +94,7 @@ function _parse_params() { upgrade=0 update=0 ovpn_option=1 + restapi_option=0 adblock_option=1 wg_option=1 insiders=0 @@ -111,6 +112,10 @@ function _parse_params() { ovpn_option="$2" shift ;; + --api|--rest|--restapi) + restapi_option="$2" + shift + ;; -a|--adblock) adblock_option="$2" shift diff --git a/installers/restapi.service b/installers/restapi.service new file mode 100644 index 00000000..ce195603 --- /dev/null +++ b/installers/restapi.service @@ -0,0 +1,14 @@ +[Unit] +Description=raspap-restapi +After=network.target + +[Service] +User=root +WorkingDirectory=/etc/raspap/api +LimitNOFILE=4096 +ExecStart=/usr/bin/python uvicorn main:app --host 0.0.0.0 --port 8081 +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target \ No newline at end of file From 04a443a514c7051c622a922e0d8d4b566213e826 Mon Sep 17 00:00:00 2001 From: NL-TCH Date: Thu, 8 Feb 2024 23:28:17 +0100 Subject: [PATCH 02/51] add --help option --- installers/raspbian.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/installers/raspbian.sh b/installers/raspbian.sh index ba43c04d..e9b649a8 100755 --- a/installers/raspbian.sh +++ b/installers/raspbian.sh @@ -40,6 +40,7 @@ OPTIONS: -y, --yes, --assume-yes Assumes "yes" as an answer to all prompts -c, --cert, --certificate Installs an SSL certificate for lighttpd -o, --openvpn Used with -y, --yes, sets OpenVPN install option (0=no install) +--rest Used with -y, --yes, sets RestAPI install option (0=no install) -a, --adblock Used with -y, --yes, sets Adblock install option (0=no install) -w, --wireguard Used with -y, --yes, sets WireGuard install option (0=no install) -e, --provider Used with -y, --yes, sets the VPN provider install option From 2ac9ba580f081e0f04541db345c3d9ef15bdd837 Mon Sep 17 00:00:00 2001 From: NL-TCH Date: Thu, 8 Feb 2024 23:46:08 +0100 Subject: [PATCH 03/51] pip fix --- installers/common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installers/common.sh b/installers/common.sh index 610d447c..41df9548 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -584,7 +584,7 @@ function _create_openvpn_scripts() { function _install_restapi() { _install_log "Installing and enabling RestAPI" sudo cp -r "$webroot_dir/api" "$raspap_dir/api" || _install_status 1 "Unable to move api folder" - python -m pip install -r "$raspap_dir/api/requirements.txt" || _install_status 1 " Unable to install pip modules" + python -m pip install -r "$raspap_dir/api/requirements.txt" --break-system-packages || _install_status 1 " Unable to install pip modules" echo "Moving restapi systemd unit control file to /lib/systemd/system/" sudo mv $webroot_dir/installers/restapi.service /lib/systemd/system/ || _install_status 1 "Unable to move restapi.service file" From 8357fcb5e49811881de170781338150dd902cea7 Mon Sep 17 00:00:00 2001 From: NL-TCH Date: Thu, 8 Feb 2024 23:57:48 +0100 Subject: [PATCH 04/51] fix typo --- installers/common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installers/common.sh b/installers/common.sh index 41df9548..dabb9338 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -57,7 +57,7 @@ function _install_raspap() { _configure_networking _prompt_install_adblock _prompt_install_openvpn - _prompt_isntall_restapi + _prompt_install_restapi _install_extra_features _prompt_install_wireguard _prompt_install_vpn_providers From c43d2ea7aba834fe1e63f39bf9e96d791f6ce50e Mon Sep 17 00:00:00 2001 From: NL-TCH Date: Fri, 9 Feb 2024 00:08:46 +0100 Subject: [PATCH 05/51] fix installer --- installers/common.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/installers/common.sh b/installers/common.sh index dabb9338..59a9bea1 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -57,7 +57,7 @@ function _install_raspap() { _configure_networking _prompt_install_adblock _prompt_install_openvpn - _prompt_install_restapi + _prompt_itall_restapi _install_extra_features _prompt_install_wireguard _prompt_install_vpn_providers @@ -513,7 +513,7 @@ function _prompt_install_restapi() { _install_status 0 "(Skipped)" else _install_restapi - elif [ "$restapi_option" == 1 ]; then + elif [ "$restapi_option" == 1 ]; then _install_restapi else echo "(Skipped)" From 08db1e5b6621ce73bad03090254d4f3cd71918c0 Mon Sep 17 00:00:00 2001 From: NL-TCH Date: Fri, 9 Feb 2024 00:38:03 +0100 Subject: [PATCH 06/51] bruh, i am stupid --- installers/common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installers/common.sh b/installers/common.sh index 59a9bea1..e402b17d 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -57,7 +57,7 @@ function _install_raspap() { _configure_networking _prompt_install_adblock _prompt_install_openvpn - _prompt_itall_restapi + _prompt_install_restapi _install_extra_features _prompt_install_wireguard _prompt_install_vpn_providers From 8c82c4133028634d18472b515980bc1284621aec Mon Sep 17 00:00:00 2001 From: NL-TCH Date: Fri, 9 Feb 2024 00:50:10 +0100 Subject: [PATCH 07/51] parsing of restapi var to common --- installers/raspbian.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installers/raspbian.sh b/installers/raspbian.sh index e9b649a8..29b14e25 100755 --- a/installers/raspbian.sh +++ b/installers/raspbian.sh @@ -95,7 +95,7 @@ function _parse_params() { upgrade=0 update=0 ovpn_option=1 - restapi_option=0 + restapi_option=1 adblock_option=1 wg_option=1 insiders=0 From ccd0d62a5a945ca53f466ba366375e63e5e18b6e Mon Sep 17 00:00:00 2001 From: NL-TCH Date: Fri, 9 Feb 2024 01:50:46 +0100 Subject: [PATCH 08/51] why am i stupid? --- installers/common.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/installers/common.sh b/installers/common.sh index e402b17d..ad56b269 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -513,6 +513,7 @@ function _prompt_install_restapi() { _install_status 0 "(Skipped)" else _install_restapi + fi elif [ "$restapi_option" == 1 ]; then _install_restapi else From 8eae2d1606af305f06e883289c8e56546297676c Mon Sep 17 00:00:00 2001 From: NL-TCH Date: Fri, 9 Feb 2024 01:56:02 +0100 Subject: [PATCH 09/51] check if python is present --- installers/common.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/installers/common.sh b/installers/common.sh index ad56b269..df65a0ae 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -585,6 +585,17 @@ function _create_openvpn_scripts() { function _install_restapi() { _install_log "Installing and enabling RestAPI" sudo cp -r "$webroot_dir/api" "$raspap_dir/api" || _install_status 1 "Unable to move api folder" + + if ! command -v python &> /dev/null; then + echo "Python is not installed. Installing Python..." + sudo apt update + sudo apt install -y python3 python3-pip + echo "Python installed successfully." + else + echo "Python is already installed." + sudo apt install python3-pip -y + + fi python -m pip install -r "$raspap_dir/api/requirements.txt" --break-system-packages || _install_status 1 " Unable to install pip modules" echo "Moving restapi systemd unit control file to /lib/systemd/system/" From aec02791352ddd9e39c2e19f4fbaf26a2ca72668 Mon Sep 17 00:00:00 2001 From: NL-TCH Date: Fri, 9 Feb 2024 01:57:59 +0100 Subject: [PATCH 10/51] change python -> python3 --- installers/common.sh | 4 ++-- installers/restapi.service | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/installers/common.sh b/installers/common.sh index df65a0ae..f5ea49f3 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -586,7 +586,7 @@ function _install_restapi() { _install_log "Installing and enabling RestAPI" sudo cp -r "$webroot_dir/api" "$raspap_dir/api" || _install_status 1 "Unable to move api folder" - if ! command -v python &> /dev/null; then + if ! command -v python3 &> /dev/null; then echo "Python is not installed. Installing Python..." sudo apt update sudo apt install -y python3 python3-pip @@ -596,7 +596,7 @@ function _install_restapi() { sudo apt install python3-pip -y fi - python -m pip install -r "$raspap_dir/api/requirements.txt" --break-system-packages || _install_status 1 " Unable to install pip modules" + python3 -m pip install -r "$raspap_dir/api/requirements.txt" --break-system-packages || _install_status 1 " Unable to install pip modules" echo "Moving restapi systemd unit control file to /lib/systemd/system/" sudo mv $webroot_dir/installers/restapi.service /lib/systemd/system/ || _install_status 1 "Unable to move restapi.service file" diff --git a/installers/restapi.service b/installers/restapi.service index ce195603..63a0d6a2 100644 --- a/installers/restapi.service +++ b/installers/restapi.service @@ -6,7 +6,7 @@ After=network.target User=root WorkingDirectory=/etc/raspap/api LimitNOFILE=4096 -ExecStart=/usr/bin/python uvicorn main:app --host 0.0.0.0 --port 8081 +ExecStart=/usr/bin/python3 uvicorn main:app --host 0.0.0.0 --port 8081 Restart=on-failure RestartSec=5s From 910616ef43a23b3e63b9172c03e202afcec35ce3 Mon Sep 17 00:00:00 2001 From: NL-TCH Date: Fri, 9 Feb 2024 02:17:15 +0100 Subject: [PATCH 11/51] fix path error in servicefile --- installers/restapi.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installers/restapi.service b/installers/restapi.service index 63a0d6a2..63748422 100644 --- a/installers/restapi.service +++ b/installers/restapi.service @@ -6,7 +6,7 @@ After=network.target User=root WorkingDirectory=/etc/raspap/api LimitNOFILE=4096 -ExecStart=/usr/bin/python3 uvicorn main:app --host 0.0.0.0 --port 8081 +ExecStart=/usr/bin/python3 -m uvicorn main:app --host 0.0.0.0 --port 8081 Restart=on-failure RestartSec=5s From 1f1fa3711af6747fcc63e0ca1ef814a831e11b59 Mon Sep 17 00:00:00 2001 From: TCH Date: Sat, 17 Feb 2024 19:45:39 +0000 Subject: [PATCH 12/51] no posts + everything behind auth --- api/main.py | 102 +++++++---------------------------------- api/modules/ap.py | 48 ------------------- api/modules/restart.py | 7 --- 3 files changed, 17 insertions(+), 140 deletions(-) delete mode 100644 api/modules/restart.py diff --git a/api/main.py b/api/main.py index a3153f0c..f5981860 100644 --- a/api/main.py +++ b/api/main.py @@ -14,18 +14,9 @@ import modules.firewall as firewall import modules.networking as networking import modules.openvpn as openvpn import modules.wireguard as wireguard -import modules.restart as restart tags_metadata = [ - { - "name": "system", - "description": "All information regarding the underlying system." - }, - { - "name": "accesspoint/hostpost", - "description": "Get and change all information regarding the hotspot" - } ] app = FastAPI( title="API for Raspap", @@ -37,7 +28,7 @@ app = FastAPI( } ) -@app.get("/system", tags=["system"]) +@app.get("/system", tags=["system"], api_key: APIKey = Depends(auth.get_api_key) async def get_system(): return{ 'hostname': system.hostname(), @@ -54,7 +45,7 @@ async def get_system(): 'rpiRevision': system.rpiRevision() } -@app.get("/ap", tags=["accesspoint/hostpost"]) +@app.get("/ap", tags=["accesspoint/hostpost"], api_key: APIKey = Depends(auth.get_api_key) async def get_ap(): return{ 'driver': ap.driver(), @@ -75,66 +66,14 @@ async def get_ap(): 'ignore_broadcast_ssid': ap.ignore_broadcast_ssid() } -@app.post("/ap", tags=["accesspoint/hostpost"]) -async def post_ap(driver=None, - ctrl_interface=None, - ctrl_interface_group=None, - auth_algs=None, - wpa_key_mgmt=None, - beacon_int=None, - ssid=None, - channel=None, - hw_mode=None, - ieee80211n=None, - wpa_passphrase=None, - interface=None, - wpa=None, - wpa_pairwise=None, - country_code=None, - ignore_broadcast_ssid=None, - api_key: APIKey = Depends(auth.get_api_key)): - if driver != None: - ap.set_driver(driver) - if ctrl_interface != None: - ap.set_ctrl_interface(ctrl_interface) - if ctrl_interface_group !=None: - ap.set_ctrl_interface_group(ctrl_interface_group) - if auth_algs != None: - ap.set_auth_algs(auth_algs) - if wpa_key_mgmt != None: - ap.set_wpa_key_mgmt(wpa_key_mgmt) - if beacon_int != None: - ap.set_beacon_int(beacon_int) - if ssid != None: - ap.set_ssid(ssid) - if channel != None: - ap.set_channel(channel) - if hw_mode != None: - ap.set_hw_mode(hw_mode) - if ieee80211n != None: - ap.set_ieee80211n(ieee80211n) - if wpa_passphrase != None: - ap.set_wpa_passphrase(wpa_passphrase) - if interface != None: - ap.set_interface(interface) - if wpa != None: - ap.set_wpa(wpa) - if wpa_pairwise != None: - ap.set_wpa_pairwise(wpa_pairwise) - if country_code != None: - ap.set_country_code(country_code) - if ignore_broadcast_ssid != None: - ap.set_ignore_broadcast_ssid(ignore_broadcast_ssid) - - -@app.get("/clients/{wireless_interface}", tags=["Clients"]) +@app.get("/clients/{wireless_interface}", tags=["Clients"], api_key: APIKey = Depends(auth.get_api_key) async def get_clients(wireless_interface): return{ 'active_clients_amount': client.get_active_clients_amount(wireless_interface), 'active_clients': json.loads(client.get_active_clients(wireless_interface)) } -@app.get("/dhcp", tags=["DHCP"]) +@app.get("/dhcp", tags=["DHCP"], api_key: APIKey = Depends(auth.get_api_key) async def get_dhcp(): return{ 'range_start': dhcp.range_start(), @@ -145,29 +84,30 @@ async def get_dhcp(): 'range_nameservers': dhcp.range_nameservers() } -@app.get("/dns/domains", tags=["DNS"]) +@app.get("/dns/domains", tags=["DNS"], api_key: APIKey = Depends(auth.get_api_key) async def get_domains(): return{ 'domains': json.loads(dns.adblockdomains()) } -@app.get("/dns/hostnames", tags=["DNS"]) + +@app.get("/dns/hostnames", tags=["DNS"], api_key: APIKey = Depends(auth.get_api_key) async def get_hostnames(): return{ 'hostnames': json.loads(dns.adblockhostnames()) } -@app.get("/dns/upstream", tags=["DNS"]) +@app.get("/dns/upstream", tags=["DNS"], api_key: APIKey = Depends(auth.get_api_key) async def get_upstream(): return{ 'upstream_nameserver': dns.upstream_nameserver() } -@app.get("/dns/logs", tags=["DNS"]) +@app.get("/dns/logs", tags=["DNS"], api_key: APIKey = Depends(auth.get_api_key) async def get_dnsmasq_logs(): return(dns.dnsmasq_logs()) -@app.get("/ddns", tags=["DDNS"]) +@app.get("/ddns", tags=["DDNS"], api_key: APIKey = Depends(auth.get_api_key) async def get_ddns(): return{ 'use': ddns.use(), @@ -179,18 +119,18 @@ async def get_ddns(): 'domain': ddns.domain() } -@app.get("/firewall", tags=["Firewall"]) +@app.get("/firewall", tags=["Firewall"], api_key: APIKey = Depends(auth.get_api_key) async def get_firewall(): return json.loads(firewall.firewall_rules()) -@app.get("/networking", tags=["Networking"]) +@app.get("/networking", tags=["Networking"], api_key: APIKey = Depends(auth.get_api_key) async def get_networking(): return{ 'interfaces': json.loads(networking.interfaces()), 'throughput': json.loads(networking.throughput()) } -@app.get("/openvpn", tags=["OpenVPN"]) +@app.get("/openvpn", tags=["OpenVPN"], api_key: APIKey = Depends(auth.get_api_key) async def get_openvpn(): return{ 'client_configs': openvpn.client_configs(), @@ -200,13 +140,13 @@ async def get_openvpn(): 'client_login_active': openvpn.client_login_active() } -@app.get("/openvpn/{config}", tags=["OpenVPN"]) +@app.get("/openvpn/{config}", tags=["OpenVPN"], api_key: APIKey = Depends(auth.get_api_key) async def client_config_list(config): return{ 'client_config': openvpn.client_config_list(config) } -@app.get("/wireguard", tags=["WireGuard"]) +@app.get("/wireguard", tags=["WireGuard"], api_key: APIKey = Depends(auth.get_api_key) async def get_wireguard(): return{ 'client_configs': wireguard.configs(), @@ -214,16 +154,8 @@ async def get_wireguard(): 'client_config_active': wireguard.client_config_active() } -@app.get("/wireguard/{config}", tags=["WireGuard"]) +@app.get("/wireguard/{config}", tags=["WireGuard"], api_key: APIKey = Depends(auth.get_api_key) async def client_config_list(config): return{ 'client_config': wireguard.client_config_list(config) -} - -@app.post("/restart/webgui") -async def restart_webgui(): - restart.webgui() - -@app.post("/restart/adblock") -async def restart_adblock(): - restart.adblock() \ No newline at end of file +} \ No newline at end of file diff --git a/api/modules/ap.py b/api/modules/ap.py index 2d6fd8e3..25dffdad 100644 --- a/api/modules/ap.py +++ b/api/modules/ap.py @@ -4,99 +4,51 @@ import json def driver(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep driver= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_driver(driver): - return subprocess.run(f"sudo sed -i 's/^driver=.*/driver={driver}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def ctrl_interface(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ctrl_interface= | cut -d'=' -f2 | head -1", shell=True, capture_output=True, text=True).stdout.strip() -def set_ctrl_interface(ctrl_interface): - return subprocess.run(f"sudo sed -i 's/^ctrl_interface=.*/ctrl_interface={ctrl_interface}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def ctrl_interface_group(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ctrl_interface_group= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_ctrl_interface_group(ctrl_interface_group): - return subprocess.run(f"sudo sed -i 's/^ctrl_interface_group=.*/ctrl_interface_group={ctrl_interface_group}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def auth_algs(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep auth_algs= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_auth_algs(auth_algs): - return subprocess.run(f"sudo sed -i 's/^auth_algs=.*/auth_algs={auth_algs}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def wpa_key_mgmt(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa_key_mgmt= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_wpa_key_mgmt(wpa_key_mgmt): - return subprocess.run(f"sudo sed -i 's/^wpa_key_mgmt=.*/wpa_key_mgmt={wpa_key_mgmt}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def beacon_int(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep beacon_int= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_beacon_int(beacon_int): - return subprocess.run(f"sudo sed -i 's/^beacon_int=.*/beacon_int={beacon_int}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def ssid(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ssid= | cut -d'=' -f2 | head -1", shell=True, capture_output=True, text=True).stdout.strip() -def set_ssid(ssid): - return subprocess.run(f"sudo sed -i 's/^ssid=.*/ssid={ssid}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def channel(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep channel= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_channel(channel): - return subprocess.run(f"sudo sed -i 's/^channel=.*/channel={channel}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def hw_mode(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep hw_mode= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_hw_mode(hw_mode): - return subprocess.run(f"sudo sed -i 's/^hw_mode=.*/hw_mode={hw_mode}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def ieee80211n(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ieee80211n= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_ieee80211n(ieee80211n): - return subprocess.run(f"sudo sed -i 's/^ieee80211n=.*/ieee80211n={ieee80211n}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def wpa_passphrase(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa_passphrase= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_wpa_passphrase(wpa_passphrase): - return subprocess.run(f"sudo sed -i 's/^wpa_passphrase=.*/wpa_passphrase={wpa_passphrase}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def interface(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep interface= | cut -d'=' -f2 | head -1", shell=True, capture_output=True, text=True).stdout.strip() -def set_interface(interface): - return subprocess.run(f"sudo sed -i 's/^interface=.*/interface={interface}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def wpa(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_wpa(wpa): - return subprocess.run(f"sudo sed -i 's/^wpa=.*/wpa={wpa}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def wpa_pairwise(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep wpa_pairwise= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_wpa_pairwise(wpa_pairwise): - return subprocess.run(f"sudo sed -i 's/^wpa_pairwise=.*/wpa_pairwise={wpa_pairwise}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def country_code(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep country_code= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_country_code(country_code): - return subprocess.run(f"sudo sed -i 's/^country_code=.*/country_code={country_code}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def ignore_broadcast_ssid(): return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ignore_broadcast_ssid= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip() -def set_ignore_broadcast_ssid(ignore_broadcast_ssid): - return subprocess.run(f"sudo sed -i 's/^ignore_broadcast_ssid=.*/ignore_broadcast_ssid={ignore_broadcast_ssid}/' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip() - def logging(): log_output = subprocess.run(f"cat /tmp/hostapd.log", shell=True, capture_output=True, text=True).stdout.strip() logs = {} diff --git a/api/modules/restart.py b/api/modules/restart.py deleted file mode 100644 index 18eb1b57..00000000 --- a/api/modules/restart.py +++ /dev/null @@ -1,7 +0,0 @@ -import subprocess - -def webgui(): - return subprocess.run("sudo /etc/raspap/lighttpd/configport.sh --restart", shell=True, capture_output=True, text=True).stdout.strip() - -def adblock(): - return subprocess.run("sudo /bin/systemctl restart dnsmasq.service", shell=True, capture_output=True, text=True).stdout.strip() \ No newline at end of file From bf41d8834044b9d81ed542faa879e8c21a04fb1d Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 19 Feb 2024 09:12:33 +0100 Subject: [PATCH 13/51] Add constant to config.php + enable with installer --- config/config.php | 1 + installers/common.sh | 2 ++ 2 files changed, 3 insertions(+) diff --git a/config/config.php b/config/config.php index a252a556..e1d1d50d 100755 --- a/config/config.php +++ b/config/config.php @@ -59,6 +59,7 @@ define('RASPI_CHANGETHEME_ENABLED', true); define('RASPI_VNSTAT_ENABLED', true); define('RASPI_SYSTEM_ENABLED', true); define('RASPI_MONITOR_ENABLED', false); +define('RASPI_RESTAPI_ENABLED', false); // Locale settings define('LOCALE_ROOT', 'locale'); diff --git a/installers/common.sh b/installers/common.sh index f5ea49f3..2d05213d 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -602,6 +602,8 @@ function _install_restapi() { sudo mv $webroot_dir/installers/restapi.service /lib/systemd/system/ || _install_status 1 "Unable to move restapi.service file" sudo systemctl daemon-reload sudo systemctl enable restapi.service || _install_status 1 "Failed to enable restapi.service" + echo "Enabling RestAPI management option" + sudo sed -i "s/\('RASPI_RESTAPI_ENABLED', \)false/\1true/g" "$webroot_dir/includes/config.php" || _install_status 1 "Unable to modify config.php" _install_status 0 } From 4e258b398113913ad78766764b24d4928ddca66e Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 19 Feb 2024 10:08:29 +0100 Subject: [PATCH 14/51] Add sidebar item, template stubs + page actions --- includes/page_actions.php | 3 +++ includes/restapi.php | 18 ++++++++++++++ includes/sidebar.php | 6 +++++ index.php | 1 + templates/restapi.php | 46 +++++++++++++++++++++++++++++++++++ templates/restapi/general.php | 0 templates/restapi/status.php | 0 7 files changed, 74 insertions(+) create mode 100644 includes/restapi.php create mode 100644 templates/restapi.php create mode 100644 templates/restapi/general.php create mode 100644 templates/restapi/status.php diff --git a/includes/page_actions.php b/includes/page_actions.php index ed64a33e..de4a1cd0 100755 --- a/includes/page_actions.php +++ b/includes/page_actions.php @@ -45,6 +45,9 @@ case "/system_info": DisplaySystem($extraFooterScripts); break; + case "/restapi_conf": + DisplayRestAPI(); + break; case "/about": DisplayAbout(); break; diff --git a/includes/restapi.php b/includes/restapi.php new file mode 100644 index 00000000..370cd02a --- /dev/null +++ b/includes/restapi.php @@ -0,0 +1,18 @@ + + + + + diff --git a/index.php b/index.php index 31a9216a..5b8b4038 100755 --- a/index.php +++ b/index.php @@ -47,6 +47,7 @@ require_once 'includes/about.php'; require_once 'includes/openvpn.php'; require_once 'includes/wireguard.php'; require_once 'includes/provider.php'; +require_once 'includes/restapi.php'; require_once 'includes/torproxy.php'; initializeApp(); diff --git a/templates/restapi.php b/templates/restapi.php new file mode 100644 index 00000000..39743df3 --- /dev/null +++ b/templates/restapi.php @@ -0,0 +1,46 @@ + + + " /> + + class="btn btn-success " name="StartRestAPIservice" value="" /> + + class="btn btn-warning " name="StopRestAPIservice" value="" /> + + + + +
+
+
+
+
+
+ +
+
+
+
+ showMessages(); ?> +
+ + + + + +
+ + +
+ + +
+
+ +
+
+
+ + diff --git a/templates/restapi/general.php b/templates/restapi/general.php new file mode 100644 index 00000000..e69de29b diff --git a/templates/restapi/status.php b/templates/restapi/status.php new file mode 100644 index 00000000..e69de29b From 2d41f74b6a79380188f30d8a84536a3e35371f1c Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 19 Feb 2024 18:31:48 +0100 Subject: [PATCH 15/51] Minor: fix caps in title --- api/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/main.py b/api/main.py index f5981860..be87903e 100644 --- a/api/main.py +++ b/api/main.py @@ -19,7 +19,7 @@ import modules.wireguard as wireguard tags_metadata = [ ] app = FastAPI( - title="API for Raspap", + title="API for RaspAP", openapi_tags=tags_metadata, version="0.0.1", license_info={ @@ -158,4 +158,4 @@ async def get_wireguard(): async def client_config_list(config): return{ 'client_config': wireguard.client_config_list(config) -} \ No newline at end of file +} From 9d7058527d71df554fe4f8c56f4565596738386a Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 19 Feb 2024 18:57:02 +0100 Subject: [PATCH 16/51] Add start/stop restapi.service to sudoers --- installers/raspap.sudoers | 2 ++ 1 file changed, 2 insertions(+) diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index 99de3b1d..ed84ad00 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -26,6 +26,8 @@ www-data ALL=(ALL) NOPASSWD:/bin/systemctl start openvpn-client@client www-data ALL=(ALL) NOPASSWD:/bin/systemctl enable openvpn-client@client www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop openvpn-client@client www-data ALL=(ALL) NOPASSWD:/bin/systemctl disable openvpn-client@client +www-data ALL=(ALL) NOPASSWD:/bin/systemctl start restapi.service +www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop restapi.service www-data ALL=(ALL) NOPASSWD:/bin/mv /tmp/ovpn/* /etc/openvpn/client/*.conf www-data ALL=(ALL) NOPASSWD:/usr/bin/ln -s /etc/openvpn/client/*.conf /etc/openvpn/client/*.conf www-data ALL=(ALL) NOPASSWD:/bin/rm /etc/openvpn/client/*.conf From 7dc2fd65382a0aac1273d7c7d85302a9f28e2589 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 19 Feb 2024 19:33:59 +0100 Subject: [PATCH 17/51] Implement basic display/save settings --- includes/restapi.php | 48 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/includes/restapi.php b/includes/restapi.php index 370cd02a..910533c2 100644 --- a/includes/restapi.php +++ b/includes/restapi.php @@ -4,15 +4,61 @@ require_once 'includes/functions.php'; require_once 'config.php'; /** - * Handler for RaspAP's RestAPI settings + * Handler for RestAPI settings */ function DisplayRestAPI() { + // initialize status object $status = new \RaspAP\Messages\StatusMessage; + // set defaults + $apiKey = "Hx80npaPTol9fKeBnPwX7ib2"; //placeholder + + if (!RASPI_MONITOR_ENABLED) { + if (isset($_POST['SaveAPIsettings'])) { + if (isset($_POST['txtapikey'])) { + $apiKey = trim($_POST['txtapikey']); + if (strlen($apiKey) == 0) { + $status->addMessage('Please enter a valid API key', 'danger'); + } else { + $return = saveAPISettings($status, $apiKey); + } + } + } elseif (isset($_POST['StartRestAPIservice'])) { + $status->addMessage('Attempting to start raspap-restapi.service', 'info'); + exec('sudo /bin/systemctl start raspap-restapi', $return); + foreach ($return as $line) { + $status->addMessage($line, 'info'); + } + } elseif (isset($_POST['StopRestAPIservice'])) { + $status->addMessage('Attempting to stop raspap-restapi.service', 'info'); + exec('sudo /bin/systemctl stop raspap-restapi.service', $return); + foreach ($return as $line) { + $status->addMessage($line, 'info'); + } + } + } + exec('pidof uvicorn | wc -l', $uvicorn); + $serviceStatus = $uvicorn[0] == 0 ? "down" : "up"; echo renderTemplate("restapi", compact( "status", "apiKey", + "serviceStatus", + "serviceLog" )); } + +/** + * Saves RestAPI settings + * + * @param object status + * @param string $apiKey + */ +function saveAPISettings($status, $apiKey) +{ + $status->addMessage('Saving API key', 'info'); + // TODO: update API key. location defined from constant + + return $status; +} From 8ecd542eae7735125e8bfa7e313f434a852b3160 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 19 Feb 2024 19:34:59 +0100 Subject: [PATCH 18/51] Create api general + status tabs --- templates/restapi.php | 12 +++++++++--- templates/restapi/general.php | 17 +++++++++++++++++ templates/restapi/status.php | 11 +++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/templates/restapi.php b/templates/restapi.php index 39743df3..0014fc6d 100644 --- a/templates/restapi.php +++ b/templates/restapi.php @@ -17,16 +17,22 @@
+
+ +
showMessages(); ?> -
+ diff --git a/templates/restapi/general.php b/templates/restapi/general.php index e69de29b..41296084 100644 --- a/templates/restapi/general.php +++ b/templates/restapi/general.php @@ -0,0 +1,17 @@ +
+

+
+
+
+
+ + +
+ +
+
+
+
+
+
+ diff --git a/templates/restapi/status.php b/templates/restapi/status.php index e69de29b..4fc87c88 100644 --- a/templates/restapi/status.php +++ b/templates/restapi/status.php @@ -0,0 +1,11 @@ + +
+

+

raspap-restapi.service status is displayed below."); ?>

+
+
+ +
+
+
+ From def8be88fbec7ddd5227fdfa5ff3fe6e849f9899 Mon Sep 17 00:00:00 2001 From: billz Date: Tue, 27 Feb 2024 11:48:37 +0100 Subject: [PATCH 19/51] Update dependencies + cp vendor/*/src src/ --- composer.json | 8 +- src/Dotenv/Dotenv.php | 267 +++++++++++ src/Dotenv/Exception/ExceptionInterface.php | 12 + .../Exception/InvalidEncodingException.php | 12 + src/Dotenv/Exception/InvalidFileException.php | 12 + src/Dotenv/Exception/InvalidPathException.php | 12 + src/Dotenv/Exception/ValidationException.php | 12 + src/Dotenv/Loader/Loader.php | 47 ++ src/Dotenv/Loader/LoaderInterface.php | 20 + src/Dotenv/Loader/Resolver.php | 65 +++ src/Dotenv/Parser/Entry.php | 59 +++ src/Dotenv/Parser/EntryParser.php | 300 ++++++++++++ src/Dotenv/Parser/Lexer.php | 58 +++ src/Dotenv/Parser/Lines.php | 127 +++++ src/Dotenv/Parser/Parser.php | 53 +++ src/Dotenv/Parser/ParserInterface.php | 19 + src/Dotenv/Parser/Value.php | 88 ++++ .../Repository/Adapter/AdapterInterface.php | 15 + .../Repository/Adapter/ApacheAdapter.php | 89 ++++ .../Repository/Adapter/ArrayAdapter.php | 80 ++++ .../Repository/Adapter/EnvConstAdapter.php | 89 ++++ .../Repository/Adapter/GuardedWriter.php | 85 ++++ .../Repository/Adapter/ImmutableWriter.php | 110 +++++ src/Dotenv/Repository/Adapter/MultiReader.php | 48 ++ src/Dotenv/Repository/Adapter/MultiWriter.php | 64 +++ .../Repository/Adapter/PutenvAdapter.php | 91 ++++ .../Repository/Adapter/ReaderInterface.php | 17 + .../Repository/Adapter/ReplacingWriter.php | 104 +++++ .../Repository/Adapter/ServerConstAdapter.php | 89 ++++ .../Repository/Adapter/WriterInterface.php | 27 ++ src/Dotenv/Repository/AdapterRepository.php | 107 +++++ src/Dotenv/Repository/RepositoryBuilder.php | 272 +++++++++++ src/Dotenv/Repository/RepositoryInterface.php | 51 ++ src/Dotenv/Store/File/Paths.php | 44 ++ src/Dotenv/Store/File/Reader.php | 81 ++++ src/Dotenv/Store/FileStore.php | 72 +++ src/Dotenv/Store/StoreBuilder.php | 141 ++++++ src/Dotenv/Store/StoreInterface.php | 17 + src/Dotenv/Store/StringStore.php | 37 ++ src/Dotenv/Util/Regex.php | 112 +++++ src/Dotenv/Util/Str.php | 98 ++++ src/Dotenv/Validator.php | 209 +++++++++ src/GrahamCampbell/ResultType/Error.php | 121 +++++ src/GrahamCampbell/ResultType/Result.php | 69 +++ src/GrahamCampbell/ResultType/Success.php | 120 +++++ src/PhpOption/LazyOption.php | 175 +++++++ src/PhpOption/None.php | 136 ++++++ src/PhpOption/Option.php | 434 ++++++++++++++++++ src/PhpOption/Some.php | 169 +++++++ 49 files changed, 4542 insertions(+), 2 deletions(-) create mode 100644 src/Dotenv/Dotenv.php create mode 100644 src/Dotenv/Exception/ExceptionInterface.php create mode 100644 src/Dotenv/Exception/InvalidEncodingException.php create mode 100644 src/Dotenv/Exception/InvalidFileException.php create mode 100644 src/Dotenv/Exception/InvalidPathException.php create mode 100644 src/Dotenv/Exception/ValidationException.php create mode 100644 src/Dotenv/Loader/Loader.php create mode 100644 src/Dotenv/Loader/LoaderInterface.php create mode 100644 src/Dotenv/Loader/Resolver.php create mode 100644 src/Dotenv/Parser/Entry.php create mode 100644 src/Dotenv/Parser/EntryParser.php create mode 100644 src/Dotenv/Parser/Lexer.php create mode 100644 src/Dotenv/Parser/Lines.php create mode 100644 src/Dotenv/Parser/Parser.php create mode 100644 src/Dotenv/Parser/ParserInterface.php create mode 100644 src/Dotenv/Parser/Value.php create mode 100644 src/Dotenv/Repository/Adapter/AdapterInterface.php create mode 100644 src/Dotenv/Repository/Adapter/ApacheAdapter.php create mode 100644 src/Dotenv/Repository/Adapter/ArrayAdapter.php create mode 100644 src/Dotenv/Repository/Adapter/EnvConstAdapter.php create mode 100644 src/Dotenv/Repository/Adapter/GuardedWriter.php create mode 100644 src/Dotenv/Repository/Adapter/ImmutableWriter.php create mode 100644 src/Dotenv/Repository/Adapter/MultiReader.php create mode 100644 src/Dotenv/Repository/Adapter/MultiWriter.php create mode 100644 src/Dotenv/Repository/Adapter/PutenvAdapter.php create mode 100644 src/Dotenv/Repository/Adapter/ReaderInterface.php create mode 100644 src/Dotenv/Repository/Adapter/ReplacingWriter.php create mode 100644 src/Dotenv/Repository/Adapter/ServerConstAdapter.php create mode 100644 src/Dotenv/Repository/Adapter/WriterInterface.php create mode 100644 src/Dotenv/Repository/AdapterRepository.php create mode 100644 src/Dotenv/Repository/RepositoryBuilder.php create mode 100644 src/Dotenv/Repository/RepositoryInterface.php create mode 100644 src/Dotenv/Store/File/Paths.php create mode 100644 src/Dotenv/Store/File/Reader.php create mode 100644 src/Dotenv/Store/FileStore.php create mode 100644 src/Dotenv/Store/StoreBuilder.php create mode 100644 src/Dotenv/Store/StoreInterface.php create mode 100644 src/Dotenv/Store/StringStore.php create mode 100644 src/Dotenv/Util/Regex.php create mode 100644 src/Dotenv/Util/Str.php create mode 100644 src/Dotenv/Validator.php create mode 100644 src/GrahamCampbell/ResultType/Error.php create mode 100644 src/GrahamCampbell/ResultType/Result.php create mode 100644 src/GrahamCampbell/ResultType/Success.php create mode 100644 src/PhpOption/LazyOption.php create mode 100644 src/PhpOption/None.php create mode 100644 src/PhpOption/Option.php create mode 100644 src/PhpOption/Some.php diff --git a/composer.json b/composer.json index ed4f5e8f..7ffa2737 100644 --- a/composer.json +++ b/composer.json @@ -13,12 +13,16 @@ } ], "require": { - "php": "^7.0" + "php": "^8.2", + "vlucas/phpdotenv": "^5.6", + "phpoption/phpoption": "^1.9", + "ext-mbstring": "*" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2.0", "phpcompatibility/php-compatibility": "^9.3.5", - "squizlabs/php_codesniffer": "^3.5.5" + "squizlabs/php_codesniffer": "^3.9.0", + "ext-simplexml": "*" }, "scripts": { "lint": "parallel-lint . --exclude vendor", diff --git a/src/Dotenv/Dotenv.php b/src/Dotenv/Dotenv.php new file mode 100644 index 00000000..0460ced2 --- /dev/null +++ b/src/Dotenv/Dotenv.php @@ -0,0 +1,267 @@ +store = $store; + $this->parser = $parser; + $this->loader = $loader; + $this->repository = $repository; + } + + /** + * Create a new dotenv instance. + * + * @param \Dotenv\Repository\RepositoryInterface $repository + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames(); + + foreach ((array) $paths as $path) { + $builder = $builder->addPath($path); + } + + foreach ((array) $names as $name) { + $builder = $builder->addName($name); + } + + if ($shortCircuit) { + $builder = $builder->shortCircuit(); + } + + return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository); + } + + /** + * Create a new mutable dotenv instance with default repository. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function createMutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters()->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Create a new mutable dotenv instance with default repository with the putenv adapter. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function createUnsafeMutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters() + ->addAdapter(PutenvAdapter::class) + ->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Create a new immutable dotenv instance with default repository. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function createImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Create a new immutable dotenv instance with default repository with the putenv adapter. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters() + ->addAdapter(PutenvAdapter::class) + ->immutable() + ->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Create a new dotenv instance with an array backed repository. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function createArrayBacked($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Parse the given content and resolve nested variables. + * + * This method behaves just like load(), only without mutating your actual + * environment. We do this by using an array backed repository. + * + * @param string $content + * + * @throws \Dotenv\Exception\InvalidFileException + * + * @return array + */ + public static function parse(string $content) + { + $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); + + $phpdotenv = new self(new StringStore($content), new Parser(), new Loader(), $repository); + + return $phpdotenv->load(); + } + + /** + * Read and load environment file(s). + * + * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException + * + * @return array + */ + public function load() + { + $entries = $this->parser->parse($this->store->read()); + + return $this->loader->load($this->repository, $entries); + } + + /** + * Read and load environment file(s), silently failing if no files can be read. + * + * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException + * + * @return array + */ + public function safeLoad() + { + try { + return $this->load(); + } catch (InvalidPathException $e) { + // suppressing exception + return []; + } + } + + /** + * Required ensures that the specified variables exist, and returns a new validator object. + * + * @param string|string[] $variables + * + * @return \Dotenv\Validator + */ + public function required($variables) + { + return (new Validator($this->repository, (array) $variables))->required(); + } + + /** + * Returns a new validator object that won't check if the specified variables exist. + * + * @param string|string[] $variables + * + * @return \Dotenv\Validator + */ + public function ifPresent($variables) + { + return new Validator($this->repository, (array) $variables); + } +} diff --git a/src/Dotenv/Exception/ExceptionInterface.php b/src/Dotenv/Exception/ExceptionInterface.php new file mode 100644 index 00000000..1e80f531 --- /dev/null +++ b/src/Dotenv/Exception/ExceptionInterface.php @@ -0,0 +1,12 @@ + + */ + public function load(RepositoryInterface $repository, array $entries) + { + return \array_reduce($entries, static function (array $vars, Entry $entry) use ($repository) { + $name = $entry->getName(); + + $value = $entry->getValue()->map(static function (Value $value) use ($repository) { + return Resolver::resolve($repository, $value); + }); + + if ($value->isDefined()) { + $inner = $value->get(); + if ($repository->set($name, $inner)) { + return \array_merge($vars, [$name => $inner]); + } + } else { + if ($repository->clear($name)) { + return \array_merge($vars, [$name => null]); + } + } + + return $vars; + }, []); + } +} diff --git a/src/Dotenv/Loader/LoaderInterface.php b/src/Dotenv/Loader/LoaderInterface.php new file mode 100644 index 00000000..275d98e8 --- /dev/null +++ b/src/Dotenv/Loader/LoaderInterface.php @@ -0,0 +1,20 @@ + + */ + public function load(RepositoryInterface $repository, array $entries); +} diff --git a/src/Dotenv/Loader/Resolver.php b/src/Dotenv/Loader/Resolver.php new file mode 100644 index 00000000..36d7a4b9 --- /dev/null +++ b/src/Dotenv/Loader/Resolver.php @@ -0,0 +1,65 @@ +getVars(), static function (string $s, int $i) use ($repository) { + return Str::substr($s, 0, $i).self::resolveVariable($repository, Str::substr($s, $i)); + }, $value->getChars()); + } + + /** + * Resolve a single nested variable. + * + * @param \Dotenv\Repository\RepositoryInterface $repository + * @param string $str + * + * @return string + */ + private static function resolveVariable(RepositoryInterface $repository, string $str) + { + return Regex::replaceCallback( + '/\A\${([a-zA-Z0-9_.]+)}/', + static function (array $matches) use ($repository) { + return Option::fromValue($repository->get($matches[1])) + ->getOrElse($matches[0]); + }, + $str, + 1 + )->success()->getOrElse($str); + } +} diff --git a/src/Dotenv/Parser/Entry.php b/src/Dotenv/Parser/Entry.php new file mode 100644 index 00000000..7570f587 --- /dev/null +++ b/src/Dotenv/Parser/Entry.php @@ -0,0 +1,59 @@ +name = $name; + $this->value = $value; + } + + /** + * Get the entry name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the entry value. + * + * @return \PhpOption\Option<\Dotenv\Parser\Value> + */ + public function getValue() + { + /** @var \PhpOption\Option<\Dotenv\Parser\Value> */ + return Option::fromValue($this->value); + } +} diff --git a/src/Dotenv/Parser/EntryParser.php b/src/Dotenv/Parser/EntryParser.php new file mode 100644 index 00000000..e286840a --- /dev/null +++ b/src/Dotenv/Parser/EntryParser.php @@ -0,0 +1,300 @@ + + */ + public static function parse(string $entry) + { + return self::splitStringIntoParts($entry)->flatMap(static function (array $parts) { + [$name, $value] = $parts; + + return self::parseName($name)->flatMap(static function (string $name) use ($value) { + /** @var Result */ + $parsedValue = $value === null ? Success::create(null) : self::parseValue($value); + + return $parsedValue->map(static function (?Value $value) use ($name) { + return new Entry($name, $value); + }); + }); + }); + } + + /** + * Split the compound string into parts. + * + * @param string $line + * + * @return \GrahamCampbell\ResultType\Result + */ + private static function splitStringIntoParts(string $line) + { + /** @var array{string,string|null} */ + $result = Str::pos($line, '=')->map(static function () use ($line) { + return \array_map('trim', \explode('=', $line, 2)); + })->getOrElse([$line, null]); + + if ($result[0] === '') { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create(self::getErrorMessage('an unexpected equals', $line)); + } + + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create($result); + } + + /** + * Parse the given variable name. + * + * That is, strip the optional quotes and leading "export" from the + * variable name. We wrap the answer in a result type. + * + * @param string $name + * + * @return \GrahamCampbell\ResultType\Result + */ + private static function parseName(string $name) + { + if (Str::len($name) > 8 && Str::substr($name, 0, 6) === 'export' && \ctype_space(Str::substr($name, 6, 1))) { + $name = \ltrim(Str::substr($name, 6)); + } + + if (self::isQuotedName($name)) { + $name = Str::substr($name, 1, -1); + } + + if (!self::isValidName($name)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create(self::getErrorMessage('an invalid name', $name)); + } + + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create($name); + } + + /** + * Is the given variable name quoted? + * + * @param string $name + * + * @return bool + */ + private static function isQuotedName(string $name) + { + if (Str::len($name) < 3) { + return false; + } + + $first = Str::substr($name, 0, 1); + $last = Str::substr($name, -1, 1); + + return ($first === '"' && $last === '"') || ($first === '\'' && $last === '\''); + } + + /** + * Is the given variable name valid? + * + * @param string $name + * + * @return bool + */ + private static function isValidName(string $name) + { + return Regex::matches('~(*UTF8)\A[\p{Ll}\p{Lu}\p{M}\p{N}_.]+\z~', $name)->success()->getOrElse(false); + } + + /** + * Parse the given variable value. + * + * This has the effect of stripping quotes and comments, dealing with + * special characters, and locating nested variables, but not resolving + * them. Formally, we run a finite state automaton with an output tape: a + * transducer. We wrap the answer in a result type. + * + * @param string $value + * + * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> + */ + private static function parseValue(string $value) + { + if (\trim($value) === '') { + /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */ + return Success::create(Value::blank()); + } + + return \array_reduce(\iterator_to_array(Lexer::lex($value)), static function (Result $data, string $token) { + return $data->flatMap(static function (array $data) use ($token) { + return self::processToken($data[1], $token)->map(static function (array $val) use ($data) { + return [$data[0]->append($val[0], $val[1]), $val[2]]; + }); + }); + }, Success::create([Value::blank(), self::INITIAL_STATE]))->flatMap(static function (array $result) { + /** @psalm-suppress DocblockTypeContradiction */ + if (in_array($result[1], self::REJECT_STATES, true)) { + /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */ + return Error::create('a missing closing quote'); + } + + /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */ + return Success::create($result[0]); + })->mapError(static function (string $err) use ($value) { + return self::getErrorMessage($err, $value); + }); + } + + /** + * Process the given token. + * + * @param int $state + * @param string $token + * + * @return \GrahamCampbell\ResultType\Result + */ + private static function processToken(int $state, string $token) + { + switch ($state) { + case self::INITIAL_STATE: + if ($token === '\'') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::SINGLE_QUOTED_STATE]); + } elseif ($token === '"') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::DOUBLE_QUOTED_STATE]); + } elseif ($token === '#') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::COMMENT_STATE]); + } elseif ($token === '$') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, true, self::UNQUOTED_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::UNQUOTED_STATE]); + } + case self::UNQUOTED_STATE: + if ($token === '#') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::COMMENT_STATE]); + } elseif (\ctype_space($token)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::WHITESPACE_STATE]); + } elseif ($token === '$') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, true, self::UNQUOTED_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::UNQUOTED_STATE]); + } + case self::SINGLE_QUOTED_STATE: + if ($token === '\'') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::WHITESPACE_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::SINGLE_QUOTED_STATE]); + } + case self::DOUBLE_QUOTED_STATE: + if ($token === '"') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::WHITESPACE_STATE]); + } elseif ($token === '\\') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::ESCAPE_SEQUENCE_STATE]); + } elseif ($token === '$') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, true, self::DOUBLE_QUOTED_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); + } + case self::ESCAPE_SEQUENCE_STATE: + if ($token === '"' || $token === '\\') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); + } elseif ($token === '$') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); + } else { + $first = Str::substr($token, 0, 1); + if (\in_array($first, ['f', 'n', 'r', 't', 'v'], true)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create([\stripcslashes('\\'.$first).Str::substr($token, 1), false, self::DOUBLE_QUOTED_STATE]); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create('an unexpected escape sequence'); + } + } + case self::WHITESPACE_STATE: + if ($token === '#') { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::COMMENT_STATE]); + } elseif (!\ctype_space($token)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create('unexpected whitespace'); + } else { + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::WHITESPACE_STATE]); + } + case self::COMMENT_STATE: + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create(['', false, self::COMMENT_STATE]); + default: + throw new \Error('Parser entered invalid state.'); + } + } + + /** + * Generate a friendly error message. + * + * @param string $cause + * @param string $subject + * + * @return string + */ + private static function getErrorMessage(string $cause, string $subject) + { + return \sprintf( + 'Encountered %s at [%s].', + $cause, + \strtok($subject, "\n") + ); + } +} diff --git a/src/Dotenv/Parser/Lexer.php b/src/Dotenv/Parser/Lexer.php new file mode 100644 index 00000000..981af24f --- /dev/null +++ b/src/Dotenv/Parser/Lexer.php @@ -0,0 +1,58 @@ + + */ + public static function lex(string $content) + { + static $regex; + + if ($regex === null) { + $regex = '(('.\implode(')|(', self::PATTERNS).'))A'; + } + + $offset = 0; + + while (isset($content[$offset])) { + if (!\preg_match($regex, $content, $matches, 0, $offset)) { + throw new \Error(\sprintf('Lexer encountered unexpected character [%s].', $content[$offset])); + } + + $offset += \strlen($matches[0]); + + yield $matches[0]; + } + } +} diff --git a/src/Dotenv/Parser/Lines.php b/src/Dotenv/Parser/Lines.php new file mode 100644 index 00000000..64979932 --- /dev/null +++ b/src/Dotenv/Parser/Lines.php @@ -0,0 +1,127 @@ +map(static function () use ($line) { + return self::looksLikeMultilineStop($line, true) === false; + })->getOrElse(false); + } + + /** + * Determine if the given line can be the start of a multiline variable. + * + * @param string $line + * @param bool $started + * + * @return bool + */ + private static function looksLikeMultilineStop(string $line, bool $started) + { + if ($line === '"') { + return true; + } + + return Regex::occurrences('/(?=([^\\\\]"))/', \str_replace('\\\\', '', $line))->map(static function (int $count) use ($started) { + return $started ? $count > 1 : $count >= 1; + })->success()->getOrElse(false); + } + + /** + * Determine if the line in the file is a comment or whitespace. + * + * @param string $line + * + * @return bool + */ + private static function isCommentOrWhitespace(string $line) + { + $line = \trim($line); + + return $line === '' || (isset($line[0]) && $line[0] === '#'); + } +} diff --git a/src/Dotenv/Parser/Parser.php b/src/Dotenv/Parser/Parser.php new file mode 100644 index 00000000..2d30dfd6 --- /dev/null +++ b/src/Dotenv/Parser/Parser.php @@ -0,0 +1,53 @@ +mapError(static function () { + return 'Could not split into separate lines.'; + })->flatMap(static function (array $lines) { + return self::process(Lines::process($lines)); + })->mapError(static function (string $error) { + throw new InvalidFileException(\sprintf('Failed to parse dotenv file. %s', $error)); + })->success()->get(); + } + + /** + * Convert the raw entries into proper entries. + * + * @param string[] $entries + * + * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[],string> + */ + private static function process(array $entries) + { + /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[],string> */ + return \array_reduce($entries, static function (Result $result, string $raw) { + return $result->flatMap(static function (array $entries) use ($raw) { + return EntryParser::parse($raw)->map(static function (Entry $entry) use ($entries) { + /** @var \Dotenv\Parser\Entry[] */ + return \array_merge($entries, [$entry]); + }); + }); + }, Success::create([])); + } +} diff --git a/src/Dotenv/Parser/ParserInterface.php b/src/Dotenv/Parser/ParserInterface.php new file mode 100644 index 00000000..17cc42ad --- /dev/null +++ b/src/Dotenv/Parser/ParserInterface.php @@ -0,0 +1,19 @@ +chars = $chars; + $this->vars = $vars; + } + + /** + * Create an empty value instance. + * + * @return \Dotenv\Parser\Value + */ + public static function blank() + { + return new self('', []); + } + + /** + * Create a new value instance, appending the characters. + * + * @param string $chars + * @param bool $var + * + * @return \Dotenv\Parser\Value + */ + public function append(string $chars, bool $var) + { + return new self( + $this->chars.$chars, + $var ? \array_merge($this->vars, [Str::len($this->chars)]) : $this->vars + ); + } + + /** + * Get the string representation of the parsed value. + * + * @return string + */ + public function getChars() + { + return $this->chars; + } + + /** + * Get the locations of the variables in the value. + * + * @return int[] + */ + public function getVars() + { + $vars = $this->vars; + + \rsort($vars); + + return $vars; + } +} diff --git a/src/Dotenv/Repository/Adapter/AdapterInterface.php b/src/Dotenv/Repository/Adapter/AdapterInterface.php new file mode 100644 index 00000000..5604398a --- /dev/null +++ b/src/Dotenv/Repository/Adapter/AdapterInterface.php @@ -0,0 +1,15 @@ + + */ + public static function create(); +} diff --git a/src/Dotenv/Repository/Adapter/ApacheAdapter.php b/src/Dotenv/Repository/Adapter/ApacheAdapter.php new file mode 100644 index 00000000..af0aae11 --- /dev/null +++ b/src/Dotenv/Repository/Adapter/ApacheAdapter.php @@ -0,0 +1,89 @@ + + */ + public static function create() + { + if (self::isSupported()) { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + return None::create(); + } + + /** + * Determines if the adapter is supported. + * + * This happens if PHP is running as an Apache module. + * + * @return bool + */ + private static function isSupported() + { + return \function_exists('apache_getenv') && \function_exists('apache_setenv'); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + /** @var \PhpOption\Option */ + return Option::fromValue(apache_getenv($name))->filter(static function ($value) { + return \is_string($value) && $value !== ''; + }); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + return apache_setenv($name, $value); + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + return apache_setenv($name, ''); + } +} diff --git a/src/Dotenv/Repository/Adapter/ArrayAdapter.php b/src/Dotenv/Repository/Adapter/ArrayAdapter.php new file mode 100644 index 00000000..df64cf6d --- /dev/null +++ b/src/Dotenv/Repository/Adapter/ArrayAdapter.php @@ -0,0 +1,80 @@ + + */ + private $variables; + + /** + * Create a new array adapter instance. + * + * @return void + */ + private function __construct() + { + $this->variables = []; + } + + /** + * Create a new instance of the adapter, if it is available. + * + * @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface> + */ + public static function create() + { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + return Option::fromArraysValue($this->variables, $name); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + $this->variables[$name] = $value; + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + unset($this->variables[$name]); + + return true; + } +} diff --git a/src/Dotenv/Repository/Adapter/EnvConstAdapter.php b/src/Dotenv/Repository/Adapter/EnvConstAdapter.php new file mode 100644 index 00000000..9eb19477 --- /dev/null +++ b/src/Dotenv/Repository/Adapter/EnvConstAdapter.php @@ -0,0 +1,89 @@ + + */ + public static function create() + { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + /** @var \PhpOption\Option */ + return Option::fromArraysValue($_ENV, $name) + ->filter(static function ($value) { + return \is_scalar($value); + }) + ->map(static function ($value) { + if ($value === false) { + return 'false'; + } + + if ($value === true) { + return 'true'; + } + + /** @psalm-suppress PossiblyInvalidCast */ + return (string) $value; + }); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + $_ENV[$name] = $value; + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + unset($_ENV[$name]); + + return true; + } +} diff --git a/src/Dotenv/Repository/Adapter/GuardedWriter.php b/src/Dotenv/Repository/Adapter/GuardedWriter.php new file mode 100644 index 00000000..fed8b9ba --- /dev/null +++ b/src/Dotenv/Repository/Adapter/GuardedWriter.php @@ -0,0 +1,85 @@ +writer = $writer; + $this->allowList = $allowList; + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + // Don't set non-allowed variables + if (!$this->isAllowed($name)) { + return false; + } + + // Set the value on the inner writer + return $this->writer->write($name, $value); + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + // Don't clear non-allowed variables + if (!$this->isAllowed($name)) { + return false; + } + + // Set the value on the inner writer + return $this->writer->delete($name); + } + + /** + * Determine if the given variable is allowed. + * + * @param non-empty-string $name + * + * @return bool + */ + private function isAllowed(string $name) + { + return \in_array($name, $this->allowList, true); + } +} diff --git a/src/Dotenv/Repository/Adapter/ImmutableWriter.php b/src/Dotenv/Repository/Adapter/ImmutableWriter.php new file mode 100644 index 00000000..399e6f9b --- /dev/null +++ b/src/Dotenv/Repository/Adapter/ImmutableWriter.php @@ -0,0 +1,110 @@ + + */ + private $loaded; + + /** + * Create a new immutable writer instance. + * + * @param \Dotenv\Repository\Adapter\WriterInterface $writer + * @param \Dotenv\Repository\Adapter\ReaderInterface $reader + * + * @return void + */ + public function __construct(WriterInterface $writer, ReaderInterface $reader) + { + $this->writer = $writer; + $this->reader = $reader; + $this->loaded = []; + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + // Don't overwrite existing environment variables + // Ruby's dotenv does this with `ENV[key] ||= value` + if ($this->isExternallyDefined($name)) { + return false; + } + + // Set the value on the inner writer + if (!$this->writer->write($name, $value)) { + return false; + } + + // Record that we have loaded the variable + $this->loaded[$name] = ''; + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + // Don't clear existing environment variables + if ($this->isExternallyDefined($name)) { + return false; + } + + // Clear the value on the inner writer + if (!$this->writer->delete($name)) { + return false; + } + + // Leave the variable as fair game + unset($this->loaded[$name]); + + return true; + } + + /** + * Determine if the given variable is externally defined. + * + * That is, is it an "existing" variable. + * + * @param non-empty-string $name + * + * @return bool + */ + private function isExternallyDefined(string $name) + { + return $this->reader->read($name)->isDefined() && !isset($this->loaded[$name]); + } +} diff --git a/src/Dotenv/Repository/Adapter/MultiReader.php b/src/Dotenv/Repository/Adapter/MultiReader.php new file mode 100644 index 00000000..0cfda6f6 --- /dev/null +++ b/src/Dotenv/Repository/Adapter/MultiReader.php @@ -0,0 +1,48 @@ +readers = $readers; + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + foreach ($this->readers as $reader) { + $result = $reader->read($name); + if ($result->isDefined()) { + return $result; + } + } + + return None::create(); + } +} diff --git a/src/Dotenv/Repository/Adapter/MultiWriter.php b/src/Dotenv/Repository/Adapter/MultiWriter.php new file mode 100644 index 00000000..15a9d8fd --- /dev/null +++ b/src/Dotenv/Repository/Adapter/MultiWriter.php @@ -0,0 +1,64 @@ +writers = $writers; + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + foreach ($this->writers as $writers) { + if (!$writers->write($name, $value)) { + return false; + } + } + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + foreach ($this->writers as $writers) { + if (!$writers->delete($name)) { + return false; + } + } + + return true; + } +} diff --git a/src/Dotenv/Repository/Adapter/PutenvAdapter.php b/src/Dotenv/Repository/Adapter/PutenvAdapter.php new file mode 100644 index 00000000..6d017cdb --- /dev/null +++ b/src/Dotenv/Repository/Adapter/PutenvAdapter.php @@ -0,0 +1,91 @@ + + */ + public static function create() + { + if (self::isSupported()) { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + return None::create(); + } + + /** + * Determines if the adapter is supported. + * + * @return bool + */ + private static function isSupported() + { + return \function_exists('getenv') && \function_exists('putenv'); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + /** @var \PhpOption\Option */ + return Option::fromValue(\getenv($name), false)->filter(static function ($value) { + return \is_string($value); + }); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + \putenv("$name=$value"); + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + \putenv($name); + + return true; + } +} diff --git a/src/Dotenv/Repository/Adapter/ReaderInterface.php b/src/Dotenv/Repository/Adapter/ReaderInterface.php new file mode 100644 index 00000000..306a63fc --- /dev/null +++ b/src/Dotenv/Repository/Adapter/ReaderInterface.php @@ -0,0 +1,17 @@ + + */ + public function read(string $name); +} diff --git a/src/Dotenv/Repository/Adapter/ReplacingWriter.php b/src/Dotenv/Repository/Adapter/ReplacingWriter.php new file mode 100644 index 00000000..98c0f041 --- /dev/null +++ b/src/Dotenv/Repository/Adapter/ReplacingWriter.php @@ -0,0 +1,104 @@ + + */ + private $seen; + + /** + * Create a new replacement writer instance. + * + * @param \Dotenv\Repository\Adapter\WriterInterface $writer + * @param \Dotenv\Repository\Adapter\ReaderInterface $reader + * + * @return void + */ + public function __construct(WriterInterface $writer, ReaderInterface $reader) + { + $this->writer = $writer; + $this->reader = $reader; + $this->seen = []; + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + if ($this->exists($name)) { + return $this->writer->write($name, $value); + } + + // succeed if nothing to do + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + if ($this->exists($name)) { + return $this->writer->delete($name); + } + + // succeed if nothing to do + return true; + } + + /** + * Does the given environment variable exist. + * + * Returns true if it currently exists, or existed at any point in the past + * that we are aware of. + * + * @param non-empty-string $name + * + * @return bool + */ + private function exists(string $name) + { + if (isset($this->seen[$name])) { + return true; + } + + if ($this->reader->read($name)->isDefined()) { + $this->seen[$name] = ''; + + return true; + } + + return false; + } +} diff --git a/src/Dotenv/Repository/Adapter/ServerConstAdapter.php b/src/Dotenv/Repository/Adapter/ServerConstAdapter.php new file mode 100644 index 00000000..f93b6e5e --- /dev/null +++ b/src/Dotenv/Repository/Adapter/ServerConstAdapter.php @@ -0,0 +1,89 @@ + + */ + public static function create() + { + /** @var \PhpOption\Option */ + return Some::create(new self()); + } + + /** + * Read an environment variable, if it exists. + * + * @param non-empty-string $name + * + * @return \PhpOption\Option + */ + public function read(string $name) + { + /** @var \PhpOption\Option */ + return Option::fromArraysValue($_SERVER, $name) + ->filter(static function ($value) { + return \is_scalar($value); + }) + ->map(static function ($value) { + if ($value === false) { + return 'false'; + } + + if ($value === true) { + return 'true'; + } + + /** @psalm-suppress PossiblyInvalidCast */ + return (string) $value; + }); + } + + /** + * Write to an environment variable, if possible. + * + * @param non-empty-string $name + * @param string $value + * + * @return bool + */ + public function write(string $name, string $value) + { + $_SERVER[$name] = $value; + + return true; + } + + /** + * Delete an environment variable, if possible. + * + * @param non-empty-string $name + * + * @return bool + */ + public function delete(string $name) + { + unset($_SERVER[$name]); + + return true; + } +} diff --git a/src/Dotenv/Repository/Adapter/WriterInterface.php b/src/Dotenv/Repository/Adapter/WriterInterface.php new file mode 100644 index 00000000..4cb3d61f --- /dev/null +++ b/src/Dotenv/Repository/Adapter/WriterInterface.php @@ -0,0 +1,27 @@ +reader = $reader; + $this->writer = $writer; + } + + /** + * Determine if the given environment variable is defined. + * + * @param string $name + * + * @return bool + */ + public function has(string $name) + { + return '' !== $name && $this->reader->read($name)->isDefined(); + } + + /** + * Get an environment variable. + * + * @param string $name + * + * @throws \InvalidArgumentException + * + * @return string|null + */ + public function get(string $name) + { + if ('' === $name) { + throw new InvalidArgumentException('Expected name to be a non-empty string.'); + } + + return $this->reader->read($name)->getOrElse(null); + } + + /** + * Set an environment variable. + * + * @param string $name + * @param string $value + * + * @throws \InvalidArgumentException + * + * @return bool + */ + public function set(string $name, string $value) + { + if ('' === $name) { + throw new InvalidArgumentException('Expected name to be a non-empty string.'); + } + + return $this->writer->write($name, $value); + } + + /** + * Clear an environment variable. + * + * @param string $name + * + * @throws \InvalidArgumentException + * + * @return bool + */ + public function clear(string $name) + { + if ('' === $name) { + throw new InvalidArgumentException('Expected name to be a non-empty string.'); + } + + return $this->writer->delete($name); + } +} diff --git a/src/Dotenv/Repository/RepositoryBuilder.php b/src/Dotenv/Repository/RepositoryBuilder.php new file mode 100644 index 00000000..a042f9a1 --- /dev/null +++ b/src/Dotenv/Repository/RepositoryBuilder.php @@ -0,0 +1,272 @@ +readers = $readers; + $this->writers = $writers; + $this->immutable = $immutable; + $this->allowList = $allowList; + } + + /** + * Create a new repository builder instance with no adapters added. + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public static function createWithNoAdapters() + { + return new self(); + } + + /** + * Create a new repository builder instance with the default adapters added. + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public static function createWithDefaultAdapters() + { + $adapters = \iterator_to_array(self::defaultAdapters()); + + return new self($adapters, $adapters); + } + + /** + * Return the array of default adapters. + * + * @return \Generator<\Dotenv\Repository\Adapter\AdapterInterface> + */ + private static function defaultAdapters() + { + foreach (self::DEFAULT_ADAPTERS as $adapter) { + $instance = $adapter::create(); + if ($instance->isDefined()) { + yield $instance->get(); + } + } + } + + /** + * Determine if the given name if of an adapterclass. + * + * @param string $name + * + * @return bool + */ + private static function isAnAdapterClass(string $name) + { + if (!\class_exists($name)) { + return false; + } + + return (new ReflectionClass($name))->implementsInterface(AdapterInterface::class); + } + + /** + * Creates a repository builder with the given reader added. + * + * Accepts either a reader instance, or a class-string for an adapter. If + * the adapter is not supported, then we silently skip adding it. + * + * @param \Dotenv\Repository\Adapter\ReaderInterface|string $reader + * + * @throws \InvalidArgumentException + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function addReader($reader) + { + if (!(\is_string($reader) && self::isAnAdapterClass($reader)) && !($reader instanceof ReaderInterface)) { + throw new InvalidArgumentException( + \sprintf( + 'Expected either an instance of %s or a class-string implementing %s', + ReaderInterface::class, + AdapterInterface::class + ) + ); + } + + $optional = Some::create($reader)->flatMap(static function ($reader) { + return \is_string($reader) ? $reader::create() : Some::create($reader); + }); + + $readers = \array_merge($this->readers, \iterator_to_array($optional)); + + return new self($readers, $this->writers, $this->immutable, $this->allowList); + } + + /** + * Creates a repository builder with the given writer added. + * + * Accepts either a writer instance, or a class-string for an adapter. If + * the adapter is not supported, then we silently skip adding it. + * + * @param \Dotenv\Repository\Adapter\WriterInterface|string $writer + * + * @throws \InvalidArgumentException + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function addWriter($writer) + { + if (!(\is_string($writer) && self::isAnAdapterClass($writer)) && !($writer instanceof WriterInterface)) { + throw new InvalidArgumentException( + \sprintf( + 'Expected either an instance of %s or a class-string implementing %s', + WriterInterface::class, + AdapterInterface::class + ) + ); + } + + $optional = Some::create($writer)->flatMap(static function ($writer) { + return \is_string($writer) ? $writer::create() : Some::create($writer); + }); + + $writers = \array_merge($this->writers, \iterator_to_array($optional)); + + return new self($this->readers, $writers, $this->immutable, $this->allowList); + } + + /** + * Creates a repository builder with the given adapter added. + * + * Accepts either an adapter instance, or a class-string for an adapter. If + * the adapter is not supported, then we silently skip adding it. We will + * add the adapter as both a reader and a writer. + * + * @param \Dotenv\Repository\Adapter\WriterInterface|string $adapter + * + * @throws \InvalidArgumentException + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function addAdapter($adapter) + { + if (!(\is_string($adapter) && self::isAnAdapterClass($adapter)) && !($adapter instanceof AdapterInterface)) { + throw new InvalidArgumentException( + \sprintf( + 'Expected either an instance of %s or a class-string implementing %s', + WriterInterface::class, + AdapterInterface::class + ) + ); + } + + $optional = Some::create($adapter)->flatMap(static function ($adapter) { + return \is_string($adapter) ? $adapter::create() : Some::create($adapter); + }); + + $readers = \array_merge($this->readers, \iterator_to_array($optional)); + $writers = \array_merge($this->writers, \iterator_to_array($optional)); + + return new self($readers, $writers, $this->immutable, $this->allowList); + } + + /** + * Creates a repository builder with mutability enabled. + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function immutable() + { + return new self($this->readers, $this->writers, true, $this->allowList); + } + + /** + * Creates a repository builder with the given allow list. + * + * @param string[]|null $allowList + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function allowList(array $allowList = null) + { + return new self($this->readers, $this->writers, $this->immutable, $allowList); + } + + /** + * Creates a new repository instance. + * + * @return \Dotenv\Repository\RepositoryInterface + */ + public function make() + { + $reader = new MultiReader($this->readers); + $writer = new MultiWriter($this->writers); + + if ($this->immutable) { + $writer = new ImmutableWriter($writer, $reader); + } + + if ($this->allowList !== null) { + $writer = new GuardedWriter($writer, $this->allowList); + } + + return new AdapterRepository($reader, $writer); + } +} diff --git a/src/Dotenv/Repository/RepositoryInterface.php b/src/Dotenv/Repository/RepositoryInterface.php new file mode 100644 index 00000000..d9b18a40 --- /dev/null +++ b/src/Dotenv/Repository/RepositoryInterface.php @@ -0,0 +1,51 @@ + + */ + public static function read(array $filePaths, bool $shortCircuit = true, string $fileEncoding = null) + { + $output = []; + + foreach ($filePaths as $filePath) { + $content = self::readFromFile($filePath, $fileEncoding); + if ($content->isDefined()) { + $output[$filePath] = $content->get(); + if ($shortCircuit) { + break; + } + } + } + + return $output; + } + + /** + * Read the given file. + * + * @param string $path + * @param string|null $encoding + * + * @throws \Dotenv\Exception\InvalidEncodingException + * + * @return \PhpOption\Option + */ + private static function readFromFile(string $path, string $encoding = null) + { + /** @var Option */ + $content = Option::fromValue(@\file_get_contents($path), false); + + return $content->flatMap(static function (string $content) use ($encoding) { + return Str::utf8($content, $encoding)->mapError(static function (string $error) { + throw new InvalidEncodingException($error); + })->success(); + }); + } +} diff --git a/src/Dotenv/Store/FileStore.php b/src/Dotenv/Store/FileStore.php new file mode 100644 index 00000000..43f6135c --- /dev/null +++ b/src/Dotenv/Store/FileStore.php @@ -0,0 +1,72 @@ +filePaths = $filePaths; + $this->shortCircuit = $shortCircuit; + $this->fileEncoding = $fileEncoding; + } + + /** + * Read the content of the environment file(s). + * + * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidPathException + * + * @return string + */ + public function read() + { + if ($this->filePaths === []) { + throw new InvalidPathException('At least one environment file path must be provided.'); + } + + $contents = Reader::read($this->filePaths, $this->shortCircuit, $this->fileEncoding); + + if (\count($contents) > 0) { + return \implode("\n", $contents); + } + + throw new InvalidPathException( + \sprintf('Unable to read any of the environment file(s) at [%s].', \implode(', ', $this->filePaths)) + ); + } +} diff --git a/src/Dotenv/Store/StoreBuilder.php b/src/Dotenv/Store/StoreBuilder.php new file mode 100644 index 00000000..304117fc --- /dev/null +++ b/src/Dotenv/Store/StoreBuilder.php @@ -0,0 +1,141 @@ +paths = $paths; + $this->names = $names; + $this->shortCircuit = $shortCircuit; + $this->fileEncoding = $fileEncoding; + } + + /** + * Create a new store builder instance with no names. + * + * @return \Dotenv\Store\StoreBuilder + */ + public static function createWithNoNames() + { + return new self(); + } + + /** + * Create a new store builder instance with the default name. + * + * @return \Dotenv\Store\StoreBuilder + */ + public static function createWithDefaultName() + { + return new self([], [self::DEFAULT_NAME]); + } + + /** + * Creates a store builder with the given path added. + * + * @param string $path + * + * @return \Dotenv\Store\StoreBuilder + */ + public function addPath(string $path) + { + return new self(\array_merge($this->paths, [$path]), $this->names, $this->shortCircuit, $this->fileEncoding); + } + + /** + * Creates a store builder with the given name added. + * + * @param string $name + * + * @return \Dotenv\Store\StoreBuilder + */ + public function addName(string $name) + { + return new self($this->paths, \array_merge($this->names, [$name]), $this->shortCircuit, $this->fileEncoding); + } + + /** + * Creates a store builder with short circuit mode enabled. + * + * @return \Dotenv\Store\StoreBuilder + */ + public function shortCircuit() + { + return new self($this->paths, $this->names, true, $this->fileEncoding); + } + + /** + * Creates a store builder with the specified file encoding. + * + * @param string|null $fileEncoding + * + * @return \Dotenv\Store\StoreBuilder + */ + public function fileEncoding(string $fileEncoding = null) + { + return new self($this->paths, $this->names, $this->shortCircuit, $fileEncoding); + } + + /** + * Creates a new store instance. + * + * @return \Dotenv\Store\StoreInterface + */ + public function make() + { + return new FileStore( + Paths::filePaths($this->paths, $this->names), + $this->shortCircuit, + $this->fileEncoding + ); + } +} diff --git a/src/Dotenv/Store/StoreInterface.php b/src/Dotenv/Store/StoreInterface.php new file mode 100644 index 00000000..6f5b9862 --- /dev/null +++ b/src/Dotenv/Store/StoreInterface.php @@ -0,0 +1,17 @@ +content = $content; + } + + /** + * Read the content of the environment file(s). + * + * @return string + */ + public function read() + { + return $this->content; + } +} diff --git a/src/Dotenv/Util/Regex.php b/src/Dotenv/Util/Regex.php new file mode 100644 index 00000000..52c15780 --- /dev/null +++ b/src/Dotenv/Util/Regex.php @@ -0,0 +1,112 @@ + + */ + public static function matches(string $pattern, string $subject) + { + return self::pregAndWrap(static function (string $subject) use ($pattern) { + return @\preg_match($pattern, $subject) === 1; + }, $subject); + } + + /** + * Perform a preg match all, wrapping up the result. + * + * @param string $pattern + * @param string $subject + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function occurrences(string $pattern, string $subject) + { + return self::pregAndWrap(static function (string $subject) use ($pattern) { + return (int) @\preg_match_all($pattern, $subject); + }, $subject); + } + + /** + * Perform a preg replace callback, wrapping up the result. + * + * @param string $pattern + * @param callable $callback + * @param string $subject + * @param int|null $limit + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function replaceCallback(string $pattern, callable $callback, string $subject, int $limit = null) + { + return self::pregAndWrap(static function (string $subject) use ($pattern, $callback, $limit) { + return (string) @\preg_replace_callback($pattern, $callback, $subject, $limit ?? -1); + }, $subject); + } + + /** + * Perform a preg split, wrapping up the result. + * + * @param string $pattern + * @param string $subject + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function split(string $pattern, string $subject) + { + return self::pregAndWrap(static function (string $subject) use ($pattern) { + /** @var string[] */ + return (array) @\preg_split($pattern, $subject); + }, $subject); + } + + /** + * Perform a preg operation, wrapping up the result. + * + * @template V + * + * @param callable(string):V $operation + * @param string $subject + * + * @return \GrahamCampbell\ResultType\Result + */ + private static function pregAndWrap(callable $operation, string $subject) + { + $result = $operation($subject); + + if (\preg_last_error() !== \PREG_NO_ERROR) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create(\preg_last_error_msg()); + } + + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create($result); + } +} diff --git a/src/Dotenv/Util/Str.php b/src/Dotenv/Util/Str.php new file mode 100644 index 00000000..087e236a --- /dev/null +++ b/src/Dotenv/Util/Str.php @@ -0,0 +1,98 @@ + + */ + public static function utf8(string $input, string $encoding = null) + { + if ($encoding !== null && !\in_array($encoding, \mb_list_encodings(), true)) { + /** @var \GrahamCampbell\ResultType\Result */ + return Error::create( + \sprintf('Illegal character encoding [%s] specified.', $encoding) + ); + } + $converted = $encoding === null ? + @\mb_convert_encoding($input, 'UTF-8') : + @\mb_convert_encoding($input, 'UTF-8', $encoding); + /** + * this is for support UTF-8 with BOM encoding + * @see https://en.wikipedia.org/wiki/Byte_order_mark + * @see https://github.com/vlucas/phpdotenv/issues/500 + */ + if (\substr($converted, 0, 3) == "\xEF\xBB\xBF") { + $converted = \substr($converted, 3); + } + /** @var \GrahamCampbell\ResultType\Result */ + return Success::create($converted); + } + + /** + * Search for a given substring of the input. + * + * @param string $haystack + * @param string $needle + * + * @return \PhpOption\Option + */ + public static function pos(string $haystack, string $needle) + { + /** @var \PhpOption\Option */ + return Option::fromValue(\mb_strpos($haystack, $needle, 0, 'UTF-8'), false); + } + + /** + * Grab the specified substring of the input. + * + * @param string $input + * @param int $start + * @param int|null $length + * + * @return string + */ + public static function substr(string $input, int $start, int $length = null) + { + return \mb_substr($input, $start, $length, 'UTF-8'); + } + + /** + * Compute the length of the given string. + * + * @param string $input + * + * @return int + */ + public static function len(string $input) + { + return \mb_strlen($input, 'UTF-8'); + } +} diff --git a/src/Dotenv/Validator.php b/src/Dotenv/Validator.php new file mode 100644 index 00000000..0c04ab62 --- /dev/null +++ b/src/Dotenv/Validator.php @@ -0,0 +1,209 @@ +repository = $repository; + $this->variables = $variables; + } + + /** + * Assert that each variable is present. + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function required() + { + return $this->assert( + static function (?string $value) { + return $value !== null; + }, + 'is missing' + ); + } + + /** + * Assert that each variable is not empty. + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function notEmpty() + { + return $this->assertNullable( + static function (string $value) { + return Str::len(\trim($value)) > 0; + }, + 'is empty' + ); + } + + /** + * Assert that each specified variable is an integer. + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function isInteger() + { + return $this->assertNullable( + static function (string $value) { + return \ctype_digit($value); + }, + 'is not an integer' + ); + } + + /** + * Assert that each specified variable is a boolean. + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function isBoolean() + { + return $this->assertNullable( + static function (string $value) { + if ($value === '') { + return false; + } + + return \filter_var($value, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE) !== null; + }, + 'is not a boolean' + ); + } + + /** + * Assert that each variable is amongst the given choices. + * + * @param string[] $choices + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function allowedValues(array $choices) + { + return $this->assertNullable( + static function (string $value) use ($choices) { + return \in_array($value, $choices, true); + }, + \sprintf('is not one of [%s]', \implode(', ', $choices)) + ); + } + + /** + * Assert that each variable matches the given regular expression. + * + * @param string $regex + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function allowedRegexValues(string $regex) + { + return $this->assertNullable( + static function (string $value) use ($regex) { + return Regex::matches($regex, $value)->success()->getOrElse(false); + }, + \sprintf('does not match "%s"', $regex) + ); + } + + /** + * Assert that the callback returns true for each variable. + * + * @param callable(?string):bool $callback + * @param string $message + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function assert(callable $callback, string $message) + { + $failing = []; + + foreach ($this->variables as $variable) { + if ($callback($this->repository->get($variable)) === false) { + $failing[] = \sprintf('%s %s', $variable, $message); + } + } + + if (\count($failing) > 0) { + throw new ValidationException(\sprintf( + 'One or more environment variables failed assertions: %s.', + \implode(', ', $failing) + )); + } + + return $this; + } + + /** + * Assert that the callback returns true for each variable. + * + * Skip checking null variable values. + * + * @param callable(string):bool $callback + * @param string $message + * + * @throws \Dotenv\Exception\ValidationException + * + * @return \Dotenv\Validator + */ + public function assertNullable(callable $callback, string $message) + { + return $this->assert( + static function (?string $value) use ($callback) { + if ($value === null) { + return true; + } + + return $callback($value); + }, + $message + ); + } +} diff --git a/src/GrahamCampbell/ResultType/Error.php b/src/GrahamCampbell/ResultType/Error.php new file mode 100644 index 00000000..2c37c3e2 --- /dev/null +++ b/src/GrahamCampbell/ResultType/Error.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace GrahamCampbell\ResultType; + +use PhpOption\None; +use PhpOption\Some; + +/** + * @template T + * @template E + * + * @extends \GrahamCampbell\ResultType\Result + */ +final class Error extends Result +{ + /** + * @var E + */ + private $value; + + /** + * Internal constructor for an error value. + * + * @param E $value + * + * @return void + */ + private function __construct($value) + { + $this->value = $value; + } + + /** + * Create a new error value. + * + * @template F + * + * @param F $value + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function create($value) + { + return new self($value); + } + + /** + * Get the success option value. + * + * @return \PhpOption\Option + */ + public function success() + { + return None::create(); + } + + /** + * Map over the success value. + * + * @template S + * + * @param callable(T):S $f + * + * @return \GrahamCampbell\ResultType\Result + */ + public function map(callable $f) + { + return self::create($this->value); + } + + /** + * Flat map over the success value. + * + * @template S + * @template F + * + * @param callable(T):\GrahamCampbell\ResultType\Result $f + * + * @return \GrahamCampbell\ResultType\Result + */ + public function flatMap(callable $f) + { + /** @var \GrahamCampbell\ResultType\Result */ + return self::create($this->value); + } + + /** + * Get the error option value. + * + * @return \PhpOption\Option + */ + public function error() + { + return Some::create($this->value); + } + + /** + * Map over the error value. + * + * @template F + * + * @param callable(E):F $f + * + * @return \GrahamCampbell\ResultType\Result + */ + public function mapError(callable $f) + { + return self::create($f($this->value)); + } +} diff --git a/src/GrahamCampbell/ResultType/Result.php b/src/GrahamCampbell/ResultType/Result.php new file mode 100644 index 00000000..8c67bcdd --- /dev/null +++ b/src/GrahamCampbell/ResultType/Result.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace GrahamCampbell\ResultType; + +/** + * @template T + * @template E + */ +abstract class Result +{ + /** + * Get the success option value. + * + * @return \PhpOption\Option + */ + abstract public function success(); + + /** + * Map over the success value. + * + * @template S + * + * @param callable(T):S $f + * + * @return \GrahamCampbell\ResultType\Result + */ + abstract public function map(callable $f); + + /** + * Flat map over the success value. + * + * @template S + * @template F + * + * @param callable(T):\GrahamCampbell\ResultType\Result $f + * + * @return \GrahamCampbell\ResultType\Result + */ + abstract public function flatMap(callable $f); + + /** + * Get the error option value. + * + * @return \PhpOption\Option + */ + abstract public function error(); + + /** + * Map over the error value. + * + * @template F + * + * @param callable(E):F $f + * + * @return \GrahamCampbell\ResultType\Result + */ + abstract public function mapError(callable $f); +} diff --git a/src/GrahamCampbell/ResultType/Success.php b/src/GrahamCampbell/ResultType/Success.php new file mode 100644 index 00000000..27cd85ee --- /dev/null +++ b/src/GrahamCampbell/ResultType/Success.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace GrahamCampbell\ResultType; + +use PhpOption\None; +use PhpOption\Some; + +/** + * @template T + * @template E + * + * @extends \GrahamCampbell\ResultType\Result + */ +final class Success extends Result +{ + /** + * @var T + */ + private $value; + + /** + * Internal constructor for a success value. + * + * @param T $value + * + * @return void + */ + private function __construct($value) + { + $this->value = $value; + } + + /** + * Create a new error value. + * + * @template S + * + * @param S $value + * + * @return \GrahamCampbell\ResultType\Result + */ + public static function create($value) + { + return new self($value); + } + + /** + * Get the success option value. + * + * @return \PhpOption\Option + */ + public function success() + { + return Some::create($this->value); + } + + /** + * Map over the success value. + * + * @template S + * + * @param callable(T):S $f + * + * @return \GrahamCampbell\ResultType\Result + */ + public function map(callable $f) + { + return self::create($f($this->value)); + } + + /** + * Flat map over the success value. + * + * @template S + * @template F + * + * @param callable(T):\GrahamCampbell\ResultType\Result $f + * + * @return \GrahamCampbell\ResultType\Result + */ + public function flatMap(callable $f) + { + return $f($this->value); + } + + /** + * Get the error option value. + * + * @return \PhpOption\Option + */ + public function error() + { + return None::create(); + } + + /** + * Map over the error value. + * + * @template F + * + * @param callable(E):F $f + * + * @return \GrahamCampbell\ResultType\Result + */ + public function mapError(callable $f) + { + return self::create($this->value); + } +} diff --git a/src/PhpOption/LazyOption.php b/src/PhpOption/LazyOption.php new file mode 100644 index 00000000..9cb77c86 --- /dev/null +++ b/src/PhpOption/LazyOption.php @@ -0,0 +1,175 @@ + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace PhpOption; + +use Traversable; + +/** + * @template T + * + * @extends Option + */ +final class LazyOption extends Option +{ + /** @var callable(mixed...):(Option) */ + private $callback; + + /** @var array */ + private $arguments; + + /** @var Option|null */ + private $option; + + /** + * @template S + * @param callable(mixed...):(Option) $callback + * @param array $arguments + * + * @return LazyOption + */ + public static function create($callback, array $arguments = []): self + { + return new self($callback, $arguments); + } + + /** + * @param callable(mixed...):(Option) $callback + * @param array $arguments + */ + public function __construct($callback, array $arguments = []) + { + if (!is_callable($callback)) { + throw new \InvalidArgumentException('Invalid callback given'); + } + + $this->callback = $callback; + $this->arguments = $arguments; + } + + public function isDefined(): bool + { + return $this->option()->isDefined(); + } + + public function isEmpty(): bool + { + return $this->option()->isEmpty(); + } + + public function get() + { + return $this->option()->get(); + } + + public function getOrElse($default) + { + return $this->option()->getOrElse($default); + } + + public function getOrCall($callable) + { + return $this->option()->getOrCall($callable); + } + + public function getOrThrow(\Exception $ex) + { + return $this->option()->getOrThrow($ex); + } + + public function orElse(Option $else) + { + return $this->option()->orElse($else); + } + + public function ifDefined($callable) + { + $this->option()->forAll($callable); + } + + public function forAll($callable) + { + return $this->option()->forAll($callable); + } + + public function map($callable) + { + return $this->option()->map($callable); + } + + public function flatMap($callable) + { + return $this->option()->flatMap($callable); + } + + public function filter($callable) + { + return $this->option()->filter($callable); + } + + public function filterNot($callable) + { + return $this->option()->filterNot($callable); + } + + public function select($value) + { + return $this->option()->select($value); + } + + public function reject($value) + { + return $this->option()->reject($value); + } + + /** + * @return Traversable + */ + public function getIterator(): Traversable + { + return $this->option()->getIterator(); + } + + public function foldLeft($initialValue, $callable) + { + return $this->option()->foldLeft($initialValue, $callable); + } + + public function foldRight($initialValue, $callable) + { + return $this->option()->foldRight($initialValue, $callable); + } + + /** + * @return Option + */ + private function option(): Option + { + if (null === $this->option) { + /** @var mixed */ + $option = call_user_func_array($this->callback, $this->arguments); + if ($option instanceof Option) { + $this->option = $option; + } else { + throw new \RuntimeException(sprintf('Expected instance of %s', Option::class)); + } + } + + return $this->option; + } +} diff --git a/src/PhpOption/None.php b/src/PhpOption/None.php new file mode 100644 index 00000000..4b85d22d --- /dev/null +++ b/src/PhpOption/None.php @@ -0,0 +1,136 @@ + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace PhpOption; + +use EmptyIterator; + +/** + * @extends Option + */ +final class None extends Option +{ + /** @var None|null */ + private static $instance; + + /** + * @return None + */ + public static function create(): self + { + if (null === self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + public function get() + { + throw new \RuntimeException('None has no value.'); + } + + public function getOrCall($callable) + { + return $callable(); + } + + public function getOrElse($default) + { + return $default; + } + + public function getOrThrow(\Exception $ex) + { + throw $ex; + } + + public function isEmpty(): bool + { + return true; + } + + public function isDefined(): bool + { + return false; + } + + public function orElse(Option $else) + { + return $else; + } + + public function ifDefined($callable) + { + // Just do nothing in that case. + } + + public function forAll($callable) + { + return $this; + } + + public function map($callable) + { + return $this; + } + + public function flatMap($callable) + { + return $this; + } + + public function filter($callable) + { + return $this; + } + + public function filterNot($callable) + { + return $this; + } + + public function select($value) + { + return $this; + } + + public function reject($value) + { + return $this; + } + + public function getIterator(): EmptyIterator + { + return new EmptyIterator(); + } + + public function foldLeft($initialValue, $callable) + { + return $initialValue; + } + + public function foldRight($initialValue, $callable) + { + return $initialValue; + } + + private function __construct() + { + } +} diff --git a/src/PhpOption/Option.php b/src/PhpOption/Option.php new file mode 100644 index 00000000..172924cf --- /dev/null +++ b/src/PhpOption/Option.php @@ -0,0 +1,434 @@ + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace PhpOption; + +use ArrayAccess; +use IteratorAggregate; + +/** + * @template T + * + * @implements IteratorAggregate + */ +abstract class Option implements IteratorAggregate +{ + /** + * Creates an option given a return value. + * + * This is intended for consuming existing APIs and allows you to easily + * convert them to an option. By default, we treat ``null`` as the None + * case, and everything else as Some. + * + * @template S + * + * @param S $value The actual return value. + * @param S $noneValue The value which should be considered "None"; null by + * default. + * + * @return Option + */ + public static function fromValue($value, $noneValue = null) + { + if ($value === $noneValue) { + return None::create(); + } + + return new Some($value); + } + + /** + * Creates an option from an array's value. + * + * If the key does not exist in the array, the array is not actually an + * array, or the array's value at the given key is null, None is returned. + * Otherwise, Some is returned wrapping the value at the given key. + * + * @template S + * + * @param array|ArrayAccess|null $array A potential array or \ArrayAccess value. + * @param string $key The key to check. + * + * @return Option + */ + public static function fromArraysValue($array, $key) + { + if (!(is_array($array) || $array instanceof ArrayAccess) || !isset($array[$key])) { + return None::create(); + } + + return new Some($array[$key]); + } + + /** + * Creates a lazy-option with the given callback. + * + * This is also a helper constructor for lazy-consuming existing APIs where + * the return value is not yet an option. By default, we treat ``null`` as + * None case, and everything else as Some. + * + * @template S + * + * @param callable $callback The callback to evaluate. + * @param array $arguments The arguments for the callback. + * @param S $noneValue The value which should be considered "None"; + * null by default. + * + * @return LazyOption + */ + public static function fromReturn($callback, array $arguments = [], $noneValue = null) + { + return new LazyOption(static function () use ($callback, $arguments, $noneValue) { + /** @var mixed */ + $return = call_user_func_array($callback, $arguments); + + if ($return === $noneValue) { + return None::create(); + } + + return new Some($return); + }); + } + + /** + * Option factory, which creates new option based on passed value. + * + * If value is already an option, it simply returns. If value is callable, + * LazyOption with passed callback created and returned. If Option + * returned from callback, it returns directly. On other case value passed + * to Option::fromValue() method. + * + * @template S + * + * @param Option|callable|S $value + * @param S $noneValue Used when $value is mixed or + * callable, for None-check. + * + * @return Option|LazyOption + */ + public static function ensure($value, $noneValue = null) + { + if ($value instanceof self) { + return $value; + } elseif (is_callable($value)) { + return new LazyOption(static function () use ($value, $noneValue) { + /** @var mixed */ + $return = $value(); + + if ($return instanceof self) { + return $return; + } else { + return self::fromValue($return, $noneValue); + } + }); + } else { + return self::fromValue($value, $noneValue); + } + } + + /** + * Lift a function so that it accepts Option as parameters. + * + * We return a new closure that wraps the original callback. If any of the + * parameters passed to the lifted function is empty, the function will + * return a value of None. Otherwise, we will pass all parameters to the + * original callback and return the value inside a new Option, unless an + * Option is returned from the function, in which case, we use that. + * + * @template S + * + * @param callable $callback + * @param mixed $noneValue + * + * @return callable + */ + public static function lift($callback, $noneValue = null) + { + return static function () use ($callback, $noneValue) { + /** @var array */ + $args = func_get_args(); + + $reduced_args = array_reduce( + $args, + /** @param bool $status */ + static function ($status, self $o) { + return $o->isEmpty() ? true : $status; + }, + false + ); + // if at least one parameter is empty, return None + if ($reduced_args) { + return None::create(); + } + + $args = array_map( + /** @return T */ + static function (self $o) { + // it is safe to do so because the fold above checked + // that all arguments are of type Some + /** @var T */ + return $o->get(); + }, + $args + ); + + return self::ensure(call_user_func_array($callback, $args), $noneValue); + }; + } + + /** + * Returns the value if available, or throws an exception otherwise. + * + * @throws \RuntimeException If value is not available. + * + * @return T + */ + abstract public function get(); + + /** + * Returns the value if available, or the default value if not. + * + * @template S + * + * @param S $default + * + * @return T|S + */ + abstract public function getOrElse($default); + + /** + * Returns the value if available, or the results of the callable. + * + * This is preferable over ``getOrElse`` if the computation of the default + * value is expensive. + * + * @template S + * + * @param callable():S $callable + * + * @return T|S + */ + abstract public function getOrCall($callable); + + /** + * Returns the value if available, or throws the passed exception. + * + * @param \Exception $ex + * + * @return T + */ + abstract public function getOrThrow(\Exception $ex); + + /** + * Returns true if no value is available, false otherwise. + * + * @return bool + */ + abstract public function isEmpty(); + + /** + * Returns true if a value is available, false otherwise. + * + * @return bool + */ + abstract public function isDefined(); + + /** + * Returns this option if non-empty, or the passed option otherwise. + * + * This can be used to try multiple alternatives, and is especially useful + * with lazy evaluating options: + * + * ```php + * $repo->findSomething() + * ->orElse(new LazyOption(array($repo, 'findSomethingElse'))) + * ->orElse(new LazyOption(array($repo, 'createSomething'))); + * ``` + * + * @param Option $else + * + * @return Option + */ + abstract public function orElse(self $else); + + /** + * This is similar to map() below except that the return value has no meaning; + * the passed callable is simply executed if the option is non-empty, and + * ignored if the option is empty. + * + * In all cases, the return value of the callable is discarded. + * + * ```php + * $comment->getMaybeFile()->ifDefined(function($file) { + * // Do something with $file here. + * }); + * ``` + * + * If you're looking for something like ``ifEmpty``, you can use ``getOrCall`` + * and ``getOrElse`` in these cases. + * + * @deprecated Use forAll() instead. + * + * @param callable(T):mixed $callable + * + * @return void + */ + abstract public function ifDefined($callable); + + /** + * This is similar to map() except that the return value of the callable has no meaning. + * + * The passed callable is simply executed if the option is non-empty, and ignored if the + * option is empty. This method is preferred for callables with side-effects, while map() + * is intended for callables without side-effects. + * + * @param callable(T):mixed $callable + * + * @return Option + */ + abstract public function forAll($callable); + + /** + * Applies the callable to the value of the option if it is non-empty, + * and returns the return value of the callable wrapped in Some(). + * + * If the option is empty, then the callable is not applied. + * + * ```php + * (new Some("foo"))->map('strtoupper')->get(); // "FOO" + * ``` + * + * @template S + * + * @param callable(T):S $callable + * + * @return Option + */ + abstract public function map($callable); + + /** + * Applies the callable to the value of the option if it is non-empty, and + * returns the return value of the callable directly. + * + * In contrast to ``map``, the return value of the callable is expected to + * be an Option itself; it is not automatically wrapped in Some(). + * + * @template S + * + * @param callable(T):Option $callable must return an Option + * + * @return Option + */ + abstract public function flatMap($callable); + + /** + * If the option is empty, it is returned immediately without applying the callable. + * + * If the option is non-empty, the callable is applied, and if it returns true, + * the option itself is returned; otherwise, None is returned. + * + * @param callable(T):bool $callable + * + * @return Option + */ + abstract public function filter($callable); + + /** + * If the option is empty, it is returned immediately without applying the callable. + * + * If the option is non-empty, the callable is applied, and if it returns false, + * the option itself is returned; otherwise, None is returned. + * + * @param callable(T):bool $callable + * + * @return Option + */ + abstract public function filterNot($callable); + + /** + * If the option is empty, it is returned immediately. + * + * If the option is non-empty, and its value does not equal the passed value + * (via a shallow comparison ===), then None is returned. Otherwise, the + * Option is returned. + * + * In other words, this will filter all but the passed value. + * + * @param T $value + * + * @return Option + */ + abstract public function select($value); + + /** + * If the option is empty, it is returned immediately. + * + * If the option is non-empty, and its value does equal the passed value (via + * a shallow comparison ===), then None is returned; otherwise, the Option is + * returned. + * + * In other words, this will let all values through except the passed value. + * + * @param T $value + * + * @return Option + */ + abstract public function reject($value); + + /** + * Binary operator for the initial value and the option's value. + * + * If empty, the initial value is returned. If non-empty, the callable + * receives the initial value and the option's value as arguments. + * + * ```php + * + * $some = new Some(5); + * $none = None::create(); + * $result = $some->foldLeft(1, function($a, $b) { return $a + $b; }); // int(6) + * $result = $none->foldLeft(1, function($a, $b) { return $a + $b; }); // int(1) + * + * // This can be used instead of something like the following: + * $option = Option::fromValue($integerOrNull); + * $result = 1; + * if ( ! $option->isEmpty()) { + * $result += $option->get(); + * } + * ``` + * + * @template S + * + * @param S $initialValue + * @param callable(S, T):S $callable + * + * @return S + */ + abstract public function foldLeft($initialValue, $callable); + + /** + * foldLeft() but with reversed arguments for the callable. + * + * @template S + * + * @param S $initialValue + * @param callable(T, S):S $callable + * + * @return S + */ + abstract public function foldRight($initialValue, $callable); +} diff --git a/src/PhpOption/Some.php b/src/PhpOption/Some.php new file mode 100644 index 00000000..032632ea --- /dev/null +++ b/src/PhpOption/Some.php @@ -0,0 +1,169 @@ + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace PhpOption; + +use ArrayIterator; + +/** + * @template T + * + * @extends Option + */ +final class Some extends Option +{ + /** @var T */ + private $value; + + /** + * @param T $value + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * @template U + * + * @param U $value + * + * @return Some + */ + public static function create($value): self + { + return new self($value); + } + + public function isDefined(): bool + { + return true; + } + + public function isEmpty(): bool + { + return false; + } + + public function get() + { + return $this->value; + } + + public function getOrElse($default) + { + return $this->value; + } + + public function getOrCall($callable) + { + return $this->value; + } + + public function getOrThrow(\Exception $ex) + { + return $this->value; + } + + public function orElse(Option $else) + { + return $this; + } + + public function ifDefined($callable) + { + $this->forAll($callable); + } + + public function forAll($callable) + { + $callable($this->value); + + return $this; + } + + public function map($callable) + { + return new self($callable($this->value)); + } + + public function flatMap($callable) + { + /** @var mixed */ + $rs = $callable($this->value); + if (!$rs instanceof Option) { + throw new \RuntimeException('Callables passed to flatMap() must return an Option. Maybe you should use map() instead?'); + } + + return $rs; + } + + public function filter($callable) + { + if (true === $callable($this->value)) { + return $this; + } + + return None::create(); + } + + public function filterNot($callable) + { + if (false === $callable($this->value)) { + return $this; + } + + return None::create(); + } + + public function select($value) + { + if ($this->value === $value) { + return $this; + } + + return None::create(); + } + + public function reject($value) + { + if ($this->value === $value) { + return None::create(); + } + + return $this; + } + + /** + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator([$this->value]); + } + + public function foldLeft($initialValue, $callable) + { + return $callable($initialValue, $this->value); + } + + public function foldRight($initialValue, $callable) + { + return $callable($this->value, $initialValue); + } +} From 4bf514c992160dcdb5b8c275568bb24fe2b27e85 Mon Sep 17 00:00:00 2001 From: billz Date: Tue, 27 Feb 2024 11:49:21 +0100 Subject: [PATCH 20/51] Add required php-mbstring to _install_dependencies --- installers/common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installers/common.sh b/installers/common.sh index 2d05213d..72c66dc1 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -250,7 +250,7 @@ function _install_dependencies() { # Set dconf-set-selections echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections - sudo apt-get install -y lighttpd git hostapd dnsmasq iptables-persistent $php_package $dhcpcd_package $iw_package $rsync_package $network_tools vnstat qrencode jq isoquery || _install_status 1 "Unable to install dependencies" + sudo apt-get install -y lighttpd git hostapd dnsmasq iptables-persistent $php_package $dhcpcd_package $iw_package $rsync_package $network_tools vnstat qrencode jq isoquery php-mbstring || _install_status 1 "Unable to install dependencies" _install_status 0 } From 5150224e6615049506857787a871e17cd5f15859 Mon Sep 17 00:00:00 2001 From: billz Date: Tue, 27 Feb 2024 11:50:32 +0100 Subject: [PATCH 21/51] Read RASPAP_API_KEY from Dotenv --- includes/restapi.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/includes/restapi.php b/includes/restapi.php index 910533c2..3a2b100c 100644 --- a/includes/restapi.php +++ b/includes/restapi.php @@ -3,6 +3,10 @@ require_once 'includes/functions.php'; require_once 'config.php'; +$env = dirname(__DIR__, 1); +$dotenv = Dotenv\Dotenv::createImmutable($env); +$dotenv->safeLoad(); + /** * Handler for RestAPI settings */ @@ -12,7 +16,7 @@ function DisplayRestAPI() $status = new \RaspAP\Messages\StatusMessage; // set defaults - $apiKey = "Hx80npaPTol9fKeBnPwX7ib2"; //placeholder + $apiKey = $_ENV['RASPAP_API_KEY']; if (!RASPI_MONITOR_ENABLED) { if (isset($_POST['SaveAPIsettings'])) { From b835243e57b0d4679dfee49ec907f3afdaf6da83 Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 28 Feb 2024 19:24:49 +0100 Subject: [PATCH 22/51] Add .env to gitignore --- src/RaspAP/DotEnv/DotEnv.php | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/RaspAP/DotEnv/DotEnv.php diff --git a/src/RaspAP/DotEnv/DotEnv.php b/src/RaspAP/DotEnv/DotEnv.php new file mode 100644 index 00000000..edf3d906 --- /dev/null +++ b/src/RaspAP/DotEnv/DotEnv.php @@ -0,0 +1,66 @@ + + * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE + */ + +declare(strict_types=1); + +namespace RaspAP\DotEnv; + +class DotEnv { + protected $envFile; + protected $data = []; + + public function __construct($envFile = '.env') { + $this->envFile = $envFile; + } + + public function load() { + if (file_exists($this->envFile)) { + $this->data = parse_ini_file($this->envFile); + foreach ($this->data as $key => $value) { + if (!getenv($key)) { + putenv("$key=$value"); + $_ENV[$key] = $value; + } + } + } else { + throw new Exception(".env file '{$this->envFile}' not found."); + } + } + + public function set($key, $value) { + $this->data[$key] = $value; + putenv("$key=$value"); + $this->store($key, $value); + } + + public function get($key) { + return getenv($key); + } + + public function getAll() { + return $this->data; + } + + public function unset($key) { + unset($_ENV[$key]); + return $this; + } + + private function store($key, $value) { + $content = file_get_contents($this->envFile); + $content = preg_replace("/^$key=.*/m", "$key=$value", $content, 1, $count); + if ($count === 0) { + // if key doesn't exist, append it + $content .= "$key=$value\n"; + } + file_put_contents($this->envFile, $content); + } +} + From cfa916632bb5497fc6ee5b0e6f142b26c3f42b06 Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 28 Feb 2024 19:27:10 +0100 Subject: [PATCH 23/51] Replace external dependencies w/ native DotEnv class --- .gitignore | 1 + composer.json | 1 - includes/restapi.php | 16 +- installers/common.sh | 2 +- src/Dotenv/Dotenv.php | 267 ----------- src/Dotenv/Exception/ExceptionInterface.php | 12 - .../Exception/InvalidEncodingException.php | 12 - src/Dotenv/Exception/InvalidFileException.php | 12 - src/Dotenv/Exception/InvalidPathException.php | 12 - src/Dotenv/Exception/ValidationException.php | 12 - src/Dotenv/Loader/Loader.php | 47 -- src/Dotenv/Loader/LoaderInterface.php | 20 - src/Dotenv/Loader/Resolver.php | 65 --- src/Dotenv/Parser/Entry.php | 59 --- src/Dotenv/Parser/EntryParser.php | 300 ------------ src/Dotenv/Parser/Lexer.php | 58 --- src/Dotenv/Parser/Lines.php | 127 ----- src/Dotenv/Parser/Parser.php | 53 --- src/Dotenv/Parser/ParserInterface.php | 19 - src/Dotenv/Parser/Value.php | 88 ---- .../Repository/Adapter/AdapterInterface.php | 15 - .../Repository/Adapter/ApacheAdapter.php | 89 ---- .../Repository/Adapter/ArrayAdapter.php | 80 ---- .../Repository/Adapter/EnvConstAdapter.php | 89 ---- .../Repository/Adapter/GuardedWriter.php | 85 ---- .../Repository/Adapter/ImmutableWriter.php | 110 ----- src/Dotenv/Repository/Adapter/MultiReader.php | 48 -- src/Dotenv/Repository/Adapter/MultiWriter.php | 64 --- .../Repository/Adapter/PutenvAdapter.php | 91 ---- .../Repository/Adapter/ReaderInterface.php | 17 - .../Repository/Adapter/ReplacingWriter.php | 104 ----- .../Repository/Adapter/ServerConstAdapter.php | 89 ---- .../Repository/Adapter/WriterInterface.php | 27 -- src/Dotenv/Repository/AdapterRepository.php | 107 ----- src/Dotenv/Repository/RepositoryBuilder.php | 272 ----------- src/Dotenv/Repository/RepositoryInterface.php | 51 -- src/Dotenv/Store/File/Paths.php | 44 -- src/Dotenv/Store/File/Reader.php | 81 ---- src/Dotenv/Store/FileStore.php | 72 --- src/Dotenv/Store/StoreBuilder.php | 141 ------ src/Dotenv/Store/StoreInterface.php | 17 - src/Dotenv/Store/StringStore.php | 37 -- src/Dotenv/Util/Regex.php | 112 ----- src/Dotenv/Util/Str.php | 98 ---- src/Dotenv/Validator.php | 209 --------- src/GrahamCampbell/ResultType/Error.php | 121 ----- src/GrahamCampbell/ResultType/Result.php | 69 --- src/GrahamCampbell/ResultType/Success.php | 120 ----- src/PhpOption/LazyOption.php | 175 ------- src/PhpOption/None.php | 136 ------ src/PhpOption/Option.php | 434 ------------------ src/PhpOption/Some.php | 169 ------- 52 files changed, 11 insertions(+), 4545 deletions(-) delete mode 100644 src/Dotenv/Dotenv.php delete mode 100644 src/Dotenv/Exception/ExceptionInterface.php delete mode 100644 src/Dotenv/Exception/InvalidEncodingException.php delete mode 100644 src/Dotenv/Exception/InvalidFileException.php delete mode 100644 src/Dotenv/Exception/InvalidPathException.php delete mode 100644 src/Dotenv/Exception/ValidationException.php delete mode 100644 src/Dotenv/Loader/Loader.php delete mode 100644 src/Dotenv/Loader/LoaderInterface.php delete mode 100644 src/Dotenv/Loader/Resolver.php delete mode 100644 src/Dotenv/Parser/Entry.php delete mode 100644 src/Dotenv/Parser/EntryParser.php delete mode 100644 src/Dotenv/Parser/Lexer.php delete mode 100644 src/Dotenv/Parser/Lines.php delete mode 100644 src/Dotenv/Parser/Parser.php delete mode 100644 src/Dotenv/Parser/ParserInterface.php delete mode 100644 src/Dotenv/Parser/Value.php delete mode 100644 src/Dotenv/Repository/Adapter/AdapterInterface.php delete mode 100644 src/Dotenv/Repository/Adapter/ApacheAdapter.php delete mode 100644 src/Dotenv/Repository/Adapter/ArrayAdapter.php delete mode 100644 src/Dotenv/Repository/Adapter/EnvConstAdapter.php delete mode 100644 src/Dotenv/Repository/Adapter/GuardedWriter.php delete mode 100644 src/Dotenv/Repository/Adapter/ImmutableWriter.php delete mode 100644 src/Dotenv/Repository/Adapter/MultiReader.php delete mode 100644 src/Dotenv/Repository/Adapter/MultiWriter.php delete mode 100644 src/Dotenv/Repository/Adapter/PutenvAdapter.php delete mode 100644 src/Dotenv/Repository/Adapter/ReaderInterface.php delete mode 100644 src/Dotenv/Repository/Adapter/ReplacingWriter.php delete mode 100644 src/Dotenv/Repository/Adapter/ServerConstAdapter.php delete mode 100644 src/Dotenv/Repository/Adapter/WriterInterface.php delete mode 100644 src/Dotenv/Repository/AdapterRepository.php delete mode 100644 src/Dotenv/Repository/RepositoryBuilder.php delete mode 100644 src/Dotenv/Repository/RepositoryInterface.php delete mode 100644 src/Dotenv/Store/File/Paths.php delete mode 100644 src/Dotenv/Store/File/Reader.php delete mode 100644 src/Dotenv/Store/FileStore.php delete mode 100644 src/Dotenv/Store/StoreBuilder.php delete mode 100644 src/Dotenv/Store/StoreInterface.php delete mode 100644 src/Dotenv/Store/StringStore.php delete mode 100644 src/Dotenv/Util/Regex.php delete mode 100644 src/Dotenv/Util/Str.php delete mode 100644 src/Dotenv/Validator.php delete mode 100644 src/GrahamCampbell/ResultType/Error.php delete mode 100644 src/GrahamCampbell/ResultType/Result.php delete mode 100644 src/GrahamCampbell/ResultType/Success.php delete mode 100644 src/PhpOption/LazyOption.php delete mode 100644 src/PhpOption/None.php delete mode 100644 src/PhpOption/Option.php delete mode 100644 src/PhpOption/Some.php diff --git a/.gitignore b/.gitignore index a2a77b49..245d6fa4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ yarn-error.log includes/config.php rootCA.pem vendor +.env diff --git a/composer.json b/composer.json index 7ffa2737..ad52f66b 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,6 @@ ], "require": { "php": "^8.2", - "vlucas/phpdotenv": "^5.6", "phpoption/phpoption": "^1.9", "ext-mbstring": "*" }, diff --git a/includes/restapi.php b/includes/restapi.php index 3a2b100c..015c0cb1 100644 --- a/includes/restapi.php +++ b/includes/restapi.php @@ -3,10 +3,6 @@ require_once 'includes/functions.php'; require_once 'config.php'; -$env = dirname(__DIR__, 1); -$dotenv = Dotenv\Dotenv::createImmutable($env); -$dotenv->safeLoad(); - /** * Handler for RestAPI settings */ @@ -15,6 +11,10 @@ function DisplayRestAPI() // initialize status object $status = new \RaspAP\Messages\StatusMessage; + // create instance of DotEnv + $dotenv = new \RaspAP\DotEnv\DotEnv; + $dotenv->load(); + // set defaults $apiKey = $_ENV['RASPAP_API_KEY']; @@ -25,7 +25,7 @@ function DisplayRestAPI() if (strlen($apiKey) == 0) { $status->addMessage('Please enter a valid API key', 'danger'); } else { - $return = saveAPISettings($status, $apiKey); + $return = saveAPISettings($status, $apiKey, $dotenv); } } } elseif (isset($_POST['StartRestAPIservice'])) { @@ -57,12 +57,14 @@ function DisplayRestAPI() * Saves RestAPI settings * * @param object status + * @param object dotenv * @param string $apiKey */ -function saveAPISettings($status, $apiKey) +function saveAPISettings($status, $apiKey, $dotenv) { $status->addMessage('Saving API key', 'info'); - // TODO: update API key. location defined from constant + $dotenv->set('RASPAP_API_KEY', $apiKey); return $status; } + diff --git a/installers/common.sh b/installers/common.sh index 72c66dc1..2d05213d 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -250,7 +250,7 @@ function _install_dependencies() { # Set dconf-set-selections echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections - sudo apt-get install -y lighttpd git hostapd dnsmasq iptables-persistent $php_package $dhcpcd_package $iw_package $rsync_package $network_tools vnstat qrencode jq isoquery php-mbstring || _install_status 1 "Unable to install dependencies" + sudo apt-get install -y lighttpd git hostapd dnsmasq iptables-persistent $php_package $dhcpcd_package $iw_package $rsync_package $network_tools vnstat qrencode jq isoquery || _install_status 1 "Unable to install dependencies" _install_status 0 } diff --git a/src/Dotenv/Dotenv.php b/src/Dotenv/Dotenv.php deleted file mode 100644 index 0460ced2..00000000 --- a/src/Dotenv/Dotenv.php +++ /dev/null @@ -1,267 +0,0 @@ -store = $store; - $this->parser = $parser; - $this->loader = $loader; - $this->repository = $repository; - } - - /** - * Create a new dotenv instance. - * - * @param \Dotenv\Repository\RepositoryInterface $repository - * @param string|string[] $paths - * @param string|string[]|null $names - * @param bool $shortCircuit - * @param string|null $fileEncoding - * - * @return \Dotenv\Dotenv - */ - public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) - { - $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames(); - - foreach ((array) $paths as $path) { - $builder = $builder->addPath($path); - } - - foreach ((array) $names as $name) { - $builder = $builder->addName($name); - } - - if ($shortCircuit) { - $builder = $builder->shortCircuit(); - } - - return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository); - } - - /** - * Create a new mutable dotenv instance with default repository. - * - * @param string|string[] $paths - * @param string|string[]|null $names - * @param bool $shortCircuit - * @param string|null $fileEncoding - * - * @return \Dotenv\Dotenv - */ - public static function createMutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) - { - $repository = RepositoryBuilder::createWithDefaultAdapters()->make(); - - return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); - } - - /** - * Create a new mutable dotenv instance with default repository with the putenv adapter. - * - * @param string|string[] $paths - * @param string|string[]|null $names - * @param bool $shortCircuit - * @param string|null $fileEncoding - * - * @return \Dotenv\Dotenv - */ - public static function createUnsafeMutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) - { - $repository = RepositoryBuilder::createWithDefaultAdapters() - ->addAdapter(PutenvAdapter::class) - ->make(); - - return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); - } - - /** - * Create a new immutable dotenv instance with default repository. - * - * @param string|string[] $paths - * @param string|string[]|null $names - * @param bool $shortCircuit - * @param string|null $fileEncoding - * - * @return \Dotenv\Dotenv - */ - public static function createImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) - { - $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); - - return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); - } - - /** - * Create a new immutable dotenv instance with default repository with the putenv adapter. - * - * @param string|string[] $paths - * @param string|string[]|null $names - * @param bool $shortCircuit - * @param string|null $fileEncoding - * - * @return \Dotenv\Dotenv - */ - public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) - { - $repository = RepositoryBuilder::createWithDefaultAdapters() - ->addAdapter(PutenvAdapter::class) - ->immutable() - ->make(); - - return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); - } - - /** - * Create a new dotenv instance with an array backed repository. - * - * @param string|string[] $paths - * @param string|string[]|null $names - * @param bool $shortCircuit - * @param string|null $fileEncoding - * - * @return \Dotenv\Dotenv - */ - public static function createArrayBacked($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) - { - $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); - - return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); - } - - /** - * Parse the given content and resolve nested variables. - * - * This method behaves just like load(), only without mutating your actual - * environment. We do this by using an array backed repository. - * - * @param string $content - * - * @throws \Dotenv\Exception\InvalidFileException - * - * @return array - */ - public static function parse(string $content) - { - $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); - - $phpdotenv = new self(new StringStore($content), new Parser(), new Loader(), $repository); - - return $phpdotenv->load(); - } - - /** - * Read and load environment file(s). - * - * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException - * - * @return array - */ - public function load() - { - $entries = $this->parser->parse($this->store->read()); - - return $this->loader->load($this->repository, $entries); - } - - /** - * Read and load environment file(s), silently failing if no files can be read. - * - * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException - * - * @return array - */ - public function safeLoad() - { - try { - return $this->load(); - } catch (InvalidPathException $e) { - // suppressing exception - return []; - } - } - - /** - * Required ensures that the specified variables exist, and returns a new validator object. - * - * @param string|string[] $variables - * - * @return \Dotenv\Validator - */ - public function required($variables) - { - return (new Validator($this->repository, (array) $variables))->required(); - } - - /** - * Returns a new validator object that won't check if the specified variables exist. - * - * @param string|string[] $variables - * - * @return \Dotenv\Validator - */ - public function ifPresent($variables) - { - return new Validator($this->repository, (array) $variables); - } -} diff --git a/src/Dotenv/Exception/ExceptionInterface.php b/src/Dotenv/Exception/ExceptionInterface.php deleted file mode 100644 index 1e80f531..00000000 --- a/src/Dotenv/Exception/ExceptionInterface.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ - public function load(RepositoryInterface $repository, array $entries) - { - return \array_reduce($entries, static function (array $vars, Entry $entry) use ($repository) { - $name = $entry->getName(); - - $value = $entry->getValue()->map(static function (Value $value) use ($repository) { - return Resolver::resolve($repository, $value); - }); - - if ($value->isDefined()) { - $inner = $value->get(); - if ($repository->set($name, $inner)) { - return \array_merge($vars, [$name => $inner]); - } - } else { - if ($repository->clear($name)) { - return \array_merge($vars, [$name => null]); - } - } - - return $vars; - }, []); - } -} diff --git a/src/Dotenv/Loader/LoaderInterface.php b/src/Dotenv/Loader/LoaderInterface.php deleted file mode 100644 index 275d98e8..00000000 --- a/src/Dotenv/Loader/LoaderInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ - public function load(RepositoryInterface $repository, array $entries); -} diff --git a/src/Dotenv/Loader/Resolver.php b/src/Dotenv/Loader/Resolver.php deleted file mode 100644 index 36d7a4b9..00000000 --- a/src/Dotenv/Loader/Resolver.php +++ /dev/null @@ -1,65 +0,0 @@ -getVars(), static function (string $s, int $i) use ($repository) { - return Str::substr($s, 0, $i).self::resolveVariable($repository, Str::substr($s, $i)); - }, $value->getChars()); - } - - /** - * Resolve a single nested variable. - * - * @param \Dotenv\Repository\RepositoryInterface $repository - * @param string $str - * - * @return string - */ - private static function resolveVariable(RepositoryInterface $repository, string $str) - { - return Regex::replaceCallback( - '/\A\${([a-zA-Z0-9_.]+)}/', - static function (array $matches) use ($repository) { - return Option::fromValue($repository->get($matches[1])) - ->getOrElse($matches[0]); - }, - $str, - 1 - )->success()->getOrElse($str); - } -} diff --git a/src/Dotenv/Parser/Entry.php b/src/Dotenv/Parser/Entry.php deleted file mode 100644 index 7570f587..00000000 --- a/src/Dotenv/Parser/Entry.php +++ /dev/null @@ -1,59 +0,0 @@ -name = $name; - $this->value = $value; - } - - /** - * Get the entry name. - * - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * Get the entry value. - * - * @return \PhpOption\Option<\Dotenv\Parser\Value> - */ - public function getValue() - { - /** @var \PhpOption\Option<\Dotenv\Parser\Value> */ - return Option::fromValue($this->value); - } -} diff --git a/src/Dotenv/Parser/EntryParser.php b/src/Dotenv/Parser/EntryParser.php deleted file mode 100644 index e286840a..00000000 --- a/src/Dotenv/Parser/EntryParser.php +++ /dev/null @@ -1,300 +0,0 @@ - - */ - public static function parse(string $entry) - { - return self::splitStringIntoParts($entry)->flatMap(static function (array $parts) { - [$name, $value] = $parts; - - return self::parseName($name)->flatMap(static function (string $name) use ($value) { - /** @var Result */ - $parsedValue = $value === null ? Success::create(null) : self::parseValue($value); - - return $parsedValue->map(static function (?Value $value) use ($name) { - return new Entry($name, $value); - }); - }); - }); - } - - /** - * Split the compound string into parts. - * - * @param string $line - * - * @return \GrahamCampbell\ResultType\Result - */ - private static function splitStringIntoParts(string $line) - { - /** @var array{string,string|null} */ - $result = Str::pos($line, '=')->map(static function () use ($line) { - return \array_map('trim', \explode('=', $line, 2)); - })->getOrElse([$line, null]); - - if ($result[0] === '') { - /** @var \GrahamCampbell\ResultType\Result */ - return Error::create(self::getErrorMessage('an unexpected equals', $line)); - } - - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create($result); - } - - /** - * Parse the given variable name. - * - * That is, strip the optional quotes and leading "export" from the - * variable name. We wrap the answer in a result type. - * - * @param string $name - * - * @return \GrahamCampbell\ResultType\Result - */ - private static function parseName(string $name) - { - if (Str::len($name) > 8 && Str::substr($name, 0, 6) === 'export' && \ctype_space(Str::substr($name, 6, 1))) { - $name = \ltrim(Str::substr($name, 6)); - } - - if (self::isQuotedName($name)) { - $name = Str::substr($name, 1, -1); - } - - if (!self::isValidName($name)) { - /** @var \GrahamCampbell\ResultType\Result */ - return Error::create(self::getErrorMessage('an invalid name', $name)); - } - - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create($name); - } - - /** - * Is the given variable name quoted? - * - * @param string $name - * - * @return bool - */ - private static function isQuotedName(string $name) - { - if (Str::len($name) < 3) { - return false; - } - - $first = Str::substr($name, 0, 1); - $last = Str::substr($name, -1, 1); - - return ($first === '"' && $last === '"') || ($first === '\'' && $last === '\''); - } - - /** - * Is the given variable name valid? - * - * @param string $name - * - * @return bool - */ - private static function isValidName(string $name) - { - return Regex::matches('~(*UTF8)\A[\p{Ll}\p{Lu}\p{M}\p{N}_.]+\z~', $name)->success()->getOrElse(false); - } - - /** - * Parse the given variable value. - * - * This has the effect of stripping quotes and comments, dealing with - * special characters, and locating nested variables, but not resolving - * them. Formally, we run a finite state automaton with an output tape: a - * transducer. We wrap the answer in a result type. - * - * @param string $value - * - * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> - */ - private static function parseValue(string $value) - { - if (\trim($value) === '') { - /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */ - return Success::create(Value::blank()); - } - - return \array_reduce(\iterator_to_array(Lexer::lex($value)), static function (Result $data, string $token) { - return $data->flatMap(static function (array $data) use ($token) { - return self::processToken($data[1], $token)->map(static function (array $val) use ($data) { - return [$data[0]->append($val[0], $val[1]), $val[2]]; - }); - }); - }, Success::create([Value::blank(), self::INITIAL_STATE]))->flatMap(static function (array $result) { - /** @psalm-suppress DocblockTypeContradiction */ - if (in_array($result[1], self::REJECT_STATES, true)) { - /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */ - return Error::create('a missing closing quote'); - } - - /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */ - return Success::create($result[0]); - })->mapError(static function (string $err) use ($value) { - return self::getErrorMessage($err, $value); - }); - } - - /** - * Process the given token. - * - * @param int $state - * @param string $token - * - * @return \GrahamCampbell\ResultType\Result - */ - private static function processToken(int $state, string $token) - { - switch ($state) { - case self::INITIAL_STATE: - if ($token === '\'') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create(['', false, self::SINGLE_QUOTED_STATE]); - } elseif ($token === '"') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create(['', false, self::DOUBLE_QUOTED_STATE]); - } elseif ($token === '#') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create(['', false, self::COMMENT_STATE]); - } elseif ($token === '$') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create([$token, true, self::UNQUOTED_STATE]); - } else { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create([$token, false, self::UNQUOTED_STATE]); - } - case self::UNQUOTED_STATE: - if ($token === '#') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create(['', false, self::COMMENT_STATE]); - } elseif (\ctype_space($token)) { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create(['', false, self::WHITESPACE_STATE]); - } elseif ($token === '$') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create([$token, true, self::UNQUOTED_STATE]); - } else { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create([$token, false, self::UNQUOTED_STATE]); - } - case self::SINGLE_QUOTED_STATE: - if ($token === '\'') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create(['', false, self::WHITESPACE_STATE]); - } else { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create([$token, false, self::SINGLE_QUOTED_STATE]); - } - case self::DOUBLE_QUOTED_STATE: - if ($token === '"') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create(['', false, self::WHITESPACE_STATE]); - } elseif ($token === '\\') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create(['', false, self::ESCAPE_SEQUENCE_STATE]); - } elseif ($token === '$') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create([$token, true, self::DOUBLE_QUOTED_STATE]); - } else { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); - } - case self::ESCAPE_SEQUENCE_STATE: - if ($token === '"' || $token === '\\') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); - } elseif ($token === '$') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); - } else { - $first = Str::substr($token, 0, 1); - if (\in_array($first, ['f', 'n', 'r', 't', 'v'], true)) { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create([\stripcslashes('\\'.$first).Str::substr($token, 1), false, self::DOUBLE_QUOTED_STATE]); - } else { - /** @var \GrahamCampbell\ResultType\Result */ - return Error::create('an unexpected escape sequence'); - } - } - case self::WHITESPACE_STATE: - if ($token === '#') { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create(['', false, self::COMMENT_STATE]); - } elseif (!\ctype_space($token)) { - /** @var \GrahamCampbell\ResultType\Result */ - return Error::create('unexpected whitespace'); - } else { - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create(['', false, self::WHITESPACE_STATE]); - } - case self::COMMENT_STATE: - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create(['', false, self::COMMENT_STATE]); - default: - throw new \Error('Parser entered invalid state.'); - } - } - - /** - * Generate a friendly error message. - * - * @param string $cause - * @param string $subject - * - * @return string - */ - private static function getErrorMessage(string $cause, string $subject) - { - return \sprintf( - 'Encountered %s at [%s].', - $cause, - \strtok($subject, "\n") - ); - } -} diff --git a/src/Dotenv/Parser/Lexer.php b/src/Dotenv/Parser/Lexer.php deleted file mode 100644 index 981af24f..00000000 --- a/src/Dotenv/Parser/Lexer.php +++ /dev/null @@ -1,58 +0,0 @@ - - */ - public static function lex(string $content) - { - static $regex; - - if ($regex === null) { - $regex = '(('.\implode(')|(', self::PATTERNS).'))A'; - } - - $offset = 0; - - while (isset($content[$offset])) { - if (!\preg_match($regex, $content, $matches, 0, $offset)) { - throw new \Error(\sprintf('Lexer encountered unexpected character [%s].', $content[$offset])); - } - - $offset += \strlen($matches[0]); - - yield $matches[0]; - } - } -} diff --git a/src/Dotenv/Parser/Lines.php b/src/Dotenv/Parser/Lines.php deleted file mode 100644 index 64979932..00000000 --- a/src/Dotenv/Parser/Lines.php +++ /dev/null @@ -1,127 +0,0 @@ -map(static function () use ($line) { - return self::looksLikeMultilineStop($line, true) === false; - })->getOrElse(false); - } - - /** - * Determine if the given line can be the start of a multiline variable. - * - * @param string $line - * @param bool $started - * - * @return bool - */ - private static function looksLikeMultilineStop(string $line, bool $started) - { - if ($line === '"') { - return true; - } - - return Regex::occurrences('/(?=([^\\\\]"))/', \str_replace('\\\\', '', $line))->map(static function (int $count) use ($started) { - return $started ? $count > 1 : $count >= 1; - })->success()->getOrElse(false); - } - - /** - * Determine if the line in the file is a comment or whitespace. - * - * @param string $line - * - * @return bool - */ - private static function isCommentOrWhitespace(string $line) - { - $line = \trim($line); - - return $line === '' || (isset($line[0]) && $line[0] === '#'); - } -} diff --git a/src/Dotenv/Parser/Parser.php b/src/Dotenv/Parser/Parser.php deleted file mode 100644 index 2d30dfd6..00000000 --- a/src/Dotenv/Parser/Parser.php +++ /dev/null @@ -1,53 +0,0 @@ -mapError(static function () { - return 'Could not split into separate lines.'; - })->flatMap(static function (array $lines) { - return self::process(Lines::process($lines)); - })->mapError(static function (string $error) { - throw new InvalidFileException(\sprintf('Failed to parse dotenv file. %s', $error)); - })->success()->get(); - } - - /** - * Convert the raw entries into proper entries. - * - * @param string[] $entries - * - * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[],string> - */ - private static function process(array $entries) - { - /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[],string> */ - return \array_reduce($entries, static function (Result $result, string $raw) { - return $result->flatMap(static function (array $entries) use ($raw) { - return EntryParser::parse($raw)->map(static function (Entry $entry) use ($entries) { - /** @var \Dotenv\Parser\Entry[] */ - return \array_merge($entries, [$entry]); - }); - }); - }, Success::create([])); - } -} diff --git a/src/Dotenv/Parser/ParserInterface.php b/src/Dotenv/Parser/ParserInterface.php deleted file mode 100644 index 17cc42ad..00000000 --- a/src/Dotenv/Parser/ParserInterface.php +++ /dev/null @@ -1,19 +0,0 @@ -chars = $chars; - $this->vars = $vars; - } - - /** - * Create an empty value instance. - * - * @return \Dotenv\Parser\Value - */ - public static function blank() - { - return new self('', []); - } - - /** - * Create a new value instance, appending the characters. - * - * @param string $chars - * @param bool $var - * - * @return \Dotenv\Parser\Value - */ - public function append(string $chars, bool $var) - { - return new self( - $this->chars.$chars, - $var ? \array_merge($this->vars, [Str::len($this->chars)]) : $this->vars - ); - } - - /** - * Get the string representation of the parsed value. - * - * @return string - */ - public function getChars() - { - return $this->chars; - } - - /** - * Get the locations of the variables in the value. - * - * @return int[] - */ - public function getVars() - { - $vars = $this->vars; - - \rsort($vars); - - return $vars; - } -} diff --git a/src/Dotenv/Repository/Adapter/AdapterInterface.php b/src/Dotenv/Repository/Adapter/AdapterInterface.php deleted file mode 100644 index 5604398a..00000000 --- a/src/Dotenv/Repository/Adapter/AdapterInterface.php +++ /dev/null @@ -1,15 +0,0 @@ - - */ - public static function create(); -} diff --git a/src/Dotenv/Repository/Adapter/ApacheAdapter.php b/src/Dotenv/Repository/Adapter/ApacheAdapter.php deleted file mode 100644 index af0aae11..00000000 --- a/src/Dotenv/Repository/Adapter/ApacheAdapter.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ - public static function create() - { - if (self::isSupported()) { - /** @var \PhpOption\Option */ - return Some::create(new self()); - } - - return None::create(); - } - - /** - * Determines if the adapter is supported. - * - * This happens if PHP is running as an Apache module. - * - * @return bool - */ - private static function isSupported() - { - return \function_exists('apache_getenv') && \function_exists('apache_setenv'); - } - - /** - * Read an environment variable, if it exists. - * - * @param non-empty-string $name - * - * @return \PhpOption\Option - */ - public function read(string $name) - { - /** @var \PhpOption\Option */ - return Option::fromValue(apache_getenv($name))->filter(static function ($value) { - return \is_string($value) && $value !== ''; - }); - } - - /** - * Write to an environment variable, if possible. - * - * @param non-empty-string $name - * @param string $value - * - * @return bool - */ - public function write(string $name, string $value) - { - return apache_setenv($name, $value); - } - - /** - * Delete an environment variable, if possible. - * - * @param non-empty-string $name - * - * @return bool - */ - public function delete(string $name) - { - return apache_setenv($name, ''); - } -} diff --git a/src/Dotenv/Repository/Adapter/ArrayAdapter.php b/src/Dotenv/Repository/Adapter/ArrayAdapter.php deleted file mode 100644 index df64cf6d..00000000 --- a/src/Dotenv/Repository/Adapter/ArrayAdapter.php +++ /dev/null @@ -1,80 +0,0 @@ - - */ - private $variables; - - /** - * Create a new array adapter instance. - * - * @return void - */ - private function __construct() - { - $this->variables = []; - } - - /** - * Create a new instance of the adapter, if it is available. - * - * @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface> - */ - public static function create() - { - /** @var \PhpOption\Option */ - return Some::create(new self()); - } - - /** - * Read an environment variable, if it exists. - * - * @param non-empty-string $name - * - * @return \PhpOption\Option - */ - public function read(string $name) - { - return Option::fromArraysValue($this->variables, $name); - } - - /** - * Write to an environment variable, if possible. - * - * @param non-empty-string $name - * @param string $value - * - * @return bool - */ - public function write(string $name, string $value) - { - $this->variables[$name] = $value; - - return true; - } - - /** - * Delete an environment variable, if possible. - * - * @param non-empty-string $name - * - * @return bool - */ - public function delete(string $name) - { - unset($this->variables[$name]); - - return true; - } -} diff --git a/src/Dotenv/Repository/Adapter/EnvConstAdapter.php b/src/Dotenv/Repository/Adapter/EnvConstAdapter.php deleted file mode 100644 index 9eb19477..00000000 --- a/src/Dotenv/Repository/Adapter/EnvConstAdapter.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ - public static function create() - { - /** @var \PhpOption\Option */ - return Some::create(new self()); - } - - /** - * Read an environment variable, if it exists. - * - * @param non-empty-string $name - * - * @return \PhpOption\Option - */ - public function read(string $name) - { - /** @var \PhpOption\Option */ - return Option::fromArraysValue($_ENV, $name) - ->filter(static function ($value) { - return \is_scalar($value); - }) - ->map(static function ($value) { - if ($value === false) { - return 'false'; - } - - if ($value === true) { - return 'true'; - } - - /** @psalm-suppress PossiblyInvalidCast */ - return (string) $value; - }); - } - - /** - * Write to an environment variable, if possible. - * - * @param non-empty-string $name - * @param string $value - * - * @return bool - */ - public function write(string $name, string $value) - { - $_ENV[$name] = $value; - - return true; - } - - /** - * Delete an environment variable, if possible. - * - * @param non-empty-string $name - * - * @return bool - */ - public function delete(string $name) - { - unset($_ENV[$name]); - - return true; - } -} diff --git a/src/Dotenv/Repository/Adapter/GuardedWriter.php b/src/Dotenv/Repository/Adapter/GuardedWriter.php deleted file mode 100644 index fed8b9ba..00000000 --- a/src/Dotenv/Repository/Adapter/GuardedWriter.php +++ /dev/null @@ -1,85 +0,0 @@ -writer = $writer; - $this->allowList = $allowList; - } - - /** - * Write to an environment variable, if possible. - * - * @param non-empty-string $name - * @param string $value - * - * @return bool - */ - public function write(string $name, string $value) - { - // Don't set non-allowed variables - if (!$this->isAllowed($name)) { - return false; - } - - // Set the value on the inner writer - return $this->writer->write($name, $value); - } - - /** - * Delete an environment variable, if possible. - * - * @param non-empty-string $name - * - * @return bool - */ - public function delete(string $name) - { - // Don't clear non-allowed variables - if (!$this->isAllowed($name)) { - return false; - } - - // Set the value on the inner writer - return $this->writer->delete($name); - } - - /** - * Determine if the given variable is allowed. - * - * @param non-empty-string $name - * - * @return bool - */ - private function isAllowed(string $name) - { - return \in_array($name, $this->allowList, true); - } -} diff --git a/src/Dotenv/Repository/Adapter/ImmutableWriter.php b/src/Dotenv/Repository/Adapter/ImmutableWriter.php deleted file mode 100644 index 399e6f9b..00000000 --- a/src/Dotenv/Repository/Adapter/ImmutableWriter.php +++ /dev/null @@ -1,110 +0,0 @@ - - */ - private $loaded; - - /** - * Create a new immutable writer instance. - * - * @param \Dotenv\Repository\Adapter\WriterInterface $writer - * @param \Dotenv\Repository\Adapter\ReaderInterface $reader - * - * @return void - */ - public function __construct(WriterInterface $writer, ReaderInterface $reader) - { - $this->writer = $writer; - $this->reader = $reader; - $this->loaded = []; - } - - /** - * Write to an environment variable, if possible. - * - * @param non-empty-string $name - * @param string $value - * - * @return bool - */ - public function write(string $name, string $value) - { - // Don't overwrite existing environment variables - // Ruby's dotenv does this with `ENV[key] ||= value` - if ($this->isExternallyDefined($name)) { - return false; - } - - // Set the value on the inner writer - if (!$this->writer->write($name, $value)) { - return false; - } - - // Record that we have loaded the variable - $this->loaded[$name] = ''; - - return true; - } - - /** - * Delete an environment variable, if possible. - * - * @param non-empty-string $name - * - * @return bool - */ - public function delete(string $name) - { - // Don't clear existing environment variables - if ($this->isExternallyDefined($name)) { - return false; - } - - // Clear the value on the inner writer - if (!$this->writer->delete($name)) { - return false; - } - - // Leave the variable as fair game - unset($this->loaded[$name]); - - return true; - } - - /** - * Determine if the given variable is externally defined. - * - * That is, is it an "existing" variable. - * - * @param non-empty-string $name - * - * @return bool - */ - private function isExternallyDefined(string $name) - { - return $this->reader->read($name)->isDefined() && !isset($this->loaded[$name]); - } -} diff --git a/src/Dotenv/Repository/Adapter/MultiReader.php b/src/Dotenv/Repository/Adapter/MultiReader.php deleted file mode 100644 index 0cfda6f6..00000000 --- a/src/Dotenv/Repository/Adapter/MultiReader.php +++ /dev/null @@ -1,48 +0,0 @@ -readers = $readers; - } - - /** - * Read an environment variable, if it exists. - * - * @param non-empty-string $name - * - * @return \PhpOption\Option - */ - public function read(string $name) - { - foreach ($this->readers as $reader) { - $result = $reader->read($name); - if ($result->isDefined()) { - return $result; - } - } - - return None::create(); - } -} diff --git a/src/Dotenv/Repository/Adapter/MultiWriter.php b/src/Dotenv/Repository/Adapter/MultiWriter.php deleted file mode 100644 index 15a9d8fd..00000000 --- a/src/Dotenv/Repository/Adapter/MultiWriter.php +++ /dev/null @@ -1,64 +0,0 @@ -writers = $writers; - } - - /** - * Write to an environment variable, if possible. - * - * @param non-empty-string $name - * @param string $value - * - * @return bool - */ - public function write(string $name, string $value) - { - foreach ($this->writers as $writers) { - if (!$writers->write($name, $value)) { - return false; - } - } - - return true; - } - - /** - * Delete an environment variable, if possible. - * - * @param non-empty-string $name - * - * @return bool - */ - public function delete(string $name) - { - foreach ($this->writers as $writers) { - if (!$writers->delete($name)) { - return false; - } - } - - return true; - } -} diff --git a/src/Dotenv/Repository/Adapter/PutenvAdapter.php b/src/Dotenv/Repository/Adapter/PutenvAdapter.php deleted file mode 100644 index 6d017cdb..00000000 --- a/src/Dotenv/Repository/Adapter/PutenvAdapter.php +++ /dev/null @@ -1,91 +0,0 @@ - - */ - public static function create() - { - if (self::isSupported()) { - /** @var \PhpOption\Option */ - return Some::create(new self()); - } - - return None::create(); - } - - /** - * Determines if the adapter is supported. - * - * @return bool - */ - private static function isSupported() - { - return \function_exists('getenv') && \function_exists('putenv'); - } - - /** - * Read an environment variable, if it exists. - * - * @param non-empty-string $name - * - * @return \PhpOption\Option - */ - public function read(string $name) - { - /** @var \PhpOption\Option */ - return Option::fromValue(\getenv($name), false)->filter(static function ($value) { - return \is_string($value); - }); - } - - /** - * Write to an environment variable, if possible. - * - * @param non-empty-string $name - * @param string $value - * - * @return bool - */ - public function write(string $name, string $value) - { - \putenv("$name=$value"); - - return true; - } - - /** - * Delete an environment variable, if possible. - * - * @param non-empty-string $name - * - * @return bool - */ - public function delete(string $name) - { - \putenv($name); - - return true; - } -} diff --git a/src/Dotenv/Repository/Adapter/ReaderInterface.php b/src/Dotenv/Repository/Adapter/ReaderInterface.php deleted file mode 100644 index 306a63fc..00000000 --- a/src/Dotenv/Repository/Adapter/ReaderInterface.php +++ /dev/null @@ -1,17 +0,0 @@ - - */ - public function read(string $name); -} diff --git a/src/Dotenv/Repository/Adapter/ReplacingWriter.php b/src/Dotenv/Repository/Adapter/ReplacingWriter.php deleted file mode 100644 index 98c0f041..00000000 --- a/src/Dotenv/Repository/Adapter/ReplacingWriter.php +++ /dev/null @@ -1,104 +0,0 @@ - - */ - private $seen; - - /** - * Create a new replacement writer instance. - * - * @param \Dotenv\Repository\Adapter\WriterInterface $writer - * @param \Dotenv\Repository\Adapter\ReaderInterface $reader - * - * @return void - */ - public function __construct(WriterInterface $writer, ReaderInterface $reader) - { - $this->writer = $writer; - $this->reader = $reader; - $this->seen = []; - } - - /** - * Write to an environment variable, if possible. - * - * @param non-empty-string $name - * @param string $value - * - * @return bool - */ - public function write(string $name, string $value) - { - if ($this->exists($name)) { - return $this->writer->write($name, $value); - } - - // succeed if nothing to do - return true; - } - - /** - * Delete an environment variable, if possible. - * - * @param non-empty-string $name - * - * @return bool - */ - public function delete(string $name) - { - if ($this->exists($name)) { - return $this->writer->delete($name); - } - - // succeed if nothing to do - return true; - } - - /** - * Does the given environment variable exist. - * - * Returns true if it currently exists, or existed at any point in the past - * that we are aware of. - * - * @param non-empty-string $name - * - * @return bool - */ - private function exists(string $name) - { - if (isset($this->seen[$name])) { - return true; - } - - if ($this->reader->read($name)->isDefined()) { - $this->seen[$name] = ''; - - return true; - } - - return false; - } -} diff --git a/src/Dotenv/Repository/Adapter/ServerConstAdapter.php b/src/Dotenv/Repository/Adapter/ServerConstAdapter.php deleted file mode 100644 index f93b6e5e..00000000 --- a/src/Dotenv/Repository/Adapter/ServerConstAdapter.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ - public static function create() - { - /** @var \PhpOption\Option */ - return Some::create(new self()); - } - - /** - * Read an environment variable, if it exists. - * - * @param non-empty-string $name - * - * @return \PhpOption\Option - */ - public function read(string $name) - { - /** @var \PhpOption\Option */ - return Option::fromArraysValue($_SERVER, $name) - ->filter(static function ($value) { - return \is_scalar($value); - }) - ->map(static function ($value) { - if ($value === false) { - return 'false'; - } - - if ($value === true) { - return 'true'; - } - - /** @psalm-suppress PossiblyInvalidCast */ - return (string) $value; - }); - } - - /** - * Write to an environment variable, if possible. - * - * @param non-empty-string $name - * @param string $value - * - * @return bool - */ - public function write(string $name, string $value) - { - $_SERVER[$name] = $value; - - return true; - } - - /** - * Delete an environment variable, if possible. - * - * @param non-empty-string $name - * - * @return bool - */ - public function delete(string $name) - { - unset($_SERVER[$name]); - - return true; - } -} diff --git a/src/Dotenv/Repository/Adapter/WriterInterface.php b/src/Dotenv/Repository/Adapter/WriterInterface.php deleted file mode 100644 index 4cb3d61f..00000000 --- a/src/Dotenv/Repository/Adapter/WriterInterface.php +++ /dev/null @@ -1,27 +0,0 @@ -reader = $reader; - $this->writer = $writer; - } - - /** - * Determine if the given environment variable is defined. - * - * @param string $name - * - * @return bool - */ - public function has(string $name) - { - return '' !== $name && $this->reader->read($name)->isDefined(); - } - - /** - * Get an environment variable. - * - * @param string $name - * - * @throws \InvalidArgumentException - * - * @return string|null - */ - public function get(string $name) - { - if ('' === $name) { - throw new InvalidArgumentException('Expected name to be a non-empty string.'); - } - - return $this->reader->read($name)->getOrElse(null); - } - - /** - * Set an environment variable. - * - * @param string $name - * @param string $value - * - * @throws \InvalidArgumentException - * - * @return bool - */ - public function set(string $name, string $value) - { - if ('' === $name) { - throw new InvalidArgumentException('Expected name to be a non-empty string.'); - } - - return $this->writer->write($name, $value); - } - - /** - * Clear an environment variable. - * - * @param string $name - * - * @throws \InvalidArgumentException - * - * @return bool - */ - public function clear(string $name) - { - if ('' === $name) { - throw new InvalidArgumentException('Expected name to be a non-empty string.'); - } - - return $this->writer->delete($name); - } -} diff --git a/src/Dotenv/Repository/RepositoryBuilder.php b/src/Dotenv/Repository/RepositoryBuilder.php deleted file mode 100644 index a042f9a1..00000000 --- a/src/Dotenv/Repository/RepositoryBuilder.php +++ /dev/null @@ -1,272 +0,0 @@ -readers = $readers; - $this->writers = $writers; - $this->immutable = $immutable; - $this->allowList = $allowList; - } - - /** - * Create a new repository builder instance with no adapters added. - * - * @return \Dotenv\Repository\RepositoryBuilder - */ - public static function createWithNoAdapters() - { - return new self(); - } - - /** - * Create a new repository builder instance with the default adapters added. - * - * @return \Dotenv\Repository\RepositoryBuilder - */ - public static function createWithDefaultAdapters() - { - $adapters = \iterator_to_array(self::defaultAdapters()); - - return new self($adapters, $adapters); - } - - /** - * Return the array of default adapters. - * - * @return \Generator<\Dotenv\Repository\Adapter\AdapterInterface> - */ - private static function defaultAdapters() - { - foreach (self::DEFAULT_ADAPTERS as $adapter) { - $instance = $adapter::create(); - if ($instance->isDefined()) { - yield $instance->get(); - } - } - } - - /** - * Determine if the given name if of an adapterclass. - * - * @param string $name - * - * @return bool - */ - private static function isAnAdapterClass(string $name) - { - if (!\class_exists($name)) { - return false; - } - - return (new ReflectionClass($name))->implementsInterface(AdapterInterface::class); - } - - /** - * Creates a repository builder with the given reader added. - * - * Accepts either a reader instance, or a class-string for an adapter. If - * the adapter is not supported, then we silently skip adding it. - * - * @param \Dotenv\Repository\Adapter\ReaderInterface|string $reader - * - * @throws \InvalidArgumentException - * - * @return \Dotenv\Repository\RepositoryBuilder - */ - public function addReader($reader) - { - if (!(\is_string($reader) && self::isAnAdapterClass($reader)) && !($reader instanceof ReaderInterface)) { - throw new InvalidArgumentException( - \sprintf( - 'Expected either an instance of %s or a class-string implementing %s', - ReaderInterface::class, - AdapterInterface::class - ) - ); - } - - $optional = Some::create($reader)->flatMap(static function ($reader) { - return \is_string($reader) ? $reader::create() : Some::create($reader); - }); - - $readers = \array_merge($this->readers, \iterator_to_array($optional)); - - return new self($readers, $this->writers, $this->immutable, $this->allowList); - } - - /** - * Creates a repository builder with the given writer added. - * - * Accepts either a writer instance, or a class-string for an adapter. If - * the adapter is not supported, then we silently skip adding it. - * - * @param \Dotenv\Repository\Adapter\WriterInterface|string $writer - * - * @throws \InvalidArgumentException - * - * @return \Dotenv\Repository\RepositoryBuilder - */ - public function addWriter($writer) - { - if (!(\is_string($writer) && self::isAnAdapterClass($writer)) && !($writer instanceof WriterInterface)) { - throw new InvalidArgumentException( - \sprintf( - 'Expected either an instance of %s or a class-string implementing %s', - WriterInterface::class, - AdapterInterface::class - ) - ); - } - - $optional = Some::create($writer)->flatMap(static function ($writer) { - return \is_string($writer) ? $writer::create() : Some::create($writer); - }); - - $writers = \array_merge($this->writers, \iterator_to_array($optional)); - - return new self($this->readers, $writers, $this->immutable, $this->allowList); - } - - /** - * Creates a repository builder with the given adapter added. - * - * Accepts either an adapter instance, or a class-string for an adapter. If - * the adapter is not supported, then we silently skip adding it. We will - * add the adapter as both a reader and a writer. - * - * @param \Dotenv\Repository\Adapter\WriterInterface|string $adapter - * - * @throws \InvalidArgumentException - * - * @return \Dotenv\Repository\RepositoryBuilder - */ - public function addAdapter($adapter) - { - if (!(\is_string($adapter) && self::isAnAdapterClass($adapter)) && !($adapter instanceof AdapterInterface)) { - throw new InvalidArgumentException( - \sprintf( - 'Expected either an instance of %s or a class-string implementing %s', - WriterInterface::class, - AdapterInterface::class - ) - ); - } - - $optional = Some::create($adapter)->flatMap(static function ($adapter) { - return \is_string($adapter) ? $adapter::create() : Some::create($adapter); - }); - - $readers = \array_merge($this->readers, \iterator_to_array($optional)); - $writers = \array_merge($this->writers, \iterator_to_array($optional)); - - return new self($readers, $writers, $this->immutable, $this->allowList); - } - - /** - * Creates a repository builder with mutability enabled. - * - * @return \Dotenv\Repository\RepositoryBuilder - */ - public function immutable() - { - return new self($this->readers, $this->writers, true, $this->allowList); - } - - /** - * Creates a repository builder with the given allow list. - * - * @param string[]|null $allowList - * - * @return \Dotenv\Repository\RepositoryBuilder - */ - public function allowList(array $allowList = null) - { - return new self($this->readers, $this->writers, $this->immutable, $allowList); - } - - /** - * Creates a new repository instance. - * - * @return \Dotenv\Repository\RepositoryInterface - */ - public function make() - { - $reader = new MultiReader($this->readers); - $writer = new MultiWriter($this->writers); - - if ($this->immutable) { - $writer = new ImmutableWriter($writer, $reader); - } - - if ($this->allowList !== null) { - $writer = new GuardedWriter($writer, $this->allowList); - } - - return new AdapterRepository($reader, $writer); - } -} diff --git a/src/Dotenv/Repository/RepositoryInterface.php b/src/Dotenv/Repository/RepositoryInterface.php deleted file mode 100644 index d9b18a40..00000000 --- a/src/Dotenv/Repository/RepositoryInterface.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ - public static function read(array $filePaths, bool $shortCircuit = true, string $fileEncoding = null) - { - $output = []; - - foreach ($filePaths as $filePath) { - $content = self::readFromFile($filePath, $fileEncoding); - if ($content->isDefined()) { - $output[$filePath] = $content->get(); - if ($shortCircuit) { - break; - } - } - } - - return $output; - } - - /** - * Read the given file. - * - * @param string $path - * @param string|null $encoding - * - * @throws \Dotenv\Exception\InvalidEncodingException - * - * @return \PhpOption\Option - */ - private static function readFromFile(string $path, string $encoding = null) - { - /** @var Option */ - $content = Option::fromValue(@\file_get_contents($path), false); - - return $content->flatMap(static function (string $content) use ($encoding) { - return Str::utf8($content, $encoding)->mapError(static function (string $error) { - throw new InvalidEncodingException($error); - })->success(); - }); - } -} diff --git a/src/Dotenv/Store/FileStore.php b/src/Dotenv/Store/FileStore.php deleted file mode 100644 index 43f6135c..00000000 --- a/src/Dotenv/Store/FileStore.php +++ /dev/null @@ -1,72 +0,0 @@ -filePaths = $filePaths; - $this->shortCircuit = $shortCircuit; - $this->fileEncoding = $fileEncoding; - } - - /** - * Read the content of the environment file(s). - * - * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidPathException - * - * @return string - */ - public function read() - { - if ($this->filePaths === []) { - throw new InvalidPathException('At least one environment file path must be provided.'); - } - - $contents = Reader::read($this->filePaths, $this->shortCircuit, $this->fileEncoding); - - if (\count($contents) > 0) { - return \implode("\n", $contents); - } - - throw new InvalidPathException( - \sprintf('Unable to read any of the environment file(s) at [%s].', \implode(', ', $this->filePaths)) - ); - } -} diff --git a/src/Dotenv/Store/StoreBuilder.php b/src/Dotenv/Store/StoreBuilder.php deleted file mode 100644 index 304117fc..00000000 --- a/src/Dotenv/Store/StoreBuilder.php +++ /dev/null @@ -1,141 +0,0 @@ -paths = $paths; - $this->names = $names; - $this->shortCircuit = $shortCircuit; - $this->fileEncoding = $fileEncoding; - } - - /** - * Create a new store builder instance with no names. - * - * @return \Dotenv\Store\StoreBuilder - */ - public static function createWithNoNames() - { - return new self(); - } - - /** - * Create a new store builder instance with the default name. - * - * @return \Dotenv\Store\StoreBuilder - */ - public static function createWithDefaultName() - { - return new self([], [self::DEFAULT_NAME]); - } - - /** - * Creates a store builder with the given path added. - * - * @param string $path - * - * @return \Dotenv\Store\StoreBuilder - */ - public function addPath(string $path) - { - return new self(\array_merge($this->paths, [$path]), $this->names, $this->shortCircuit, $this->fileEncoding); - } - - /** - * Creates a store builder with the given name added. - * - * @param string $name - * - * @return \Dotenv\Store\StoreBuilder - */ - public function addName(string $name) - { - return new self($this->paths, \array_merge($this->names, [$name]), $this->shortCircuit, $this->fileEncoding); - } - - /** - * Creates a store builder with short circuit mode enabled. - * - * @return \Dotenv\Store\StoreBuilder - */ - public function shortCircuit() - { - return new self($this->paths, $this->names, true, $this->fileEncoding); - } - - /** - * Creates a store builder with the specified file encoding. - * - * @param string|null $fileEncoding - * - * @return \Dotenv\Store\StoreBuilder - */ - public function fileEncoding(string $fileEncoding = null) - { - return new self($this->paths, $this->names, $this->shortCircuit, $fileEncoding); - } - - /** - * Creates a new store instance. - * - * @return \Dotenv\Store\StoreInterface - */ - public function make() - { - return new FileStore( - Paths::filePaths($this->paths, $this->names), - $this->shortCircuit, - $this->fileEncoding - ); - } -} diff --git a/src/Dotenv/Store/StoreInterface.php b/src/Dotenv/Store/StoreInterface.php deleted file mode 100644 index 6f5b9862..00000000 --- a/src/Dotenv/Store/StoreInterface.php +++ /dev/null @@ -1,17 +0,0 @@ -content = $content; - } - - /** - * Read the content of the environment file(s). - * - * @return string - */ - public function read() - { - return $this->content; - } -} diff --git a/src/Dotenv/Util/Regex.php b/src/Dotenv/Util/Regex.php deleted file mode 100644 index 52c15780..00000000 --- a/src/Dotenv/Util/Regex.php +++ /dev/null @@ -1,112 +0,0 @@ - - */ - public static function matches(string $pattern, string $subject) - { - return self::pregAndWrap(static function (string $subject) use ($pattern) { - return @\preg_match($pattern, $subject) === 1; - }, $subject); - } - - /** - * Perform a preg match all, wrapping up the result. - * - * @param string $pattern - * @param string $subject - * - * @return \GrahamCampbell\ResultType\Result - */ - public static function occurrences(string $pattern, string $subject) - { - return self::pregAndWrap(static function (string $subject) use ($pattern) { - return (int) @\preg_match_all($pattern, $subject); - }, $subject); - } - - /** - * Perform a preg replace callback, wrapping up the result. - * - * @param string $pattern - * @param callable $callback - * @param string $subject - * @param int|null $limit - * - * @return \GrahamCampbell\ResultType\Result - */ - public static function replaceCallback(string $pattern, callable $callback, string $subject, int $limit = null) - { - return self::pregAndWrap(static function (string $subject) use ($pattern, $callback, $limit) { - return (string) @\preg_replace_callback($pattern, $callback, $subject, $limit ?? -1); - }, $subject); - } - - /** - * Perform a preg split, wrapping up the result. - * - * @param string $pattern - * @param string $subject - * - * @return \GrahamCampbell\ResultType\Result - */ - public static function split(string $pattern, string $subject) - { - return self::pregAndWrap(static function (string $subject) use ($pattern) { - /** @var string[] */ - return (array) @\preg_split($pattern, $subject); - }, $subject); - } - - /** - * Perform a preg operation, wrapping up the result. - * - * @template V - * - * @param callable(string):V $operation - * @param string $subject - * - * @return \GrahamCampbell\ResultType\Result - */ - private static function pregAndWrap(callable $operation, string $subject) - { - $result = $operation($subject); - - if (\preg_last_error() !== \PREG_NO_ERROR) { - /** @var \GrahamCampbell\ResultType\Result */ - return Error::create(\preg_last_error_msg()); - } - - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create($result); - } -} diff --git a/src/Dotenv/Util/Str.php b/src/Dotenv/Util/Str.php deleted file mode 100644 index 087e236a..00000000 --- a/src/Dotenv/Util/Str.php +++ /dev/null @@ -1,98 +0,0 @@ - - */ - public static function utf8(string $input, string $encoding = null) - { - if ($encoding !== null && !\in_array($encoding, \mb_list_encodings(), true)) { - /** @var \GrahamCampbell\ResultType\Result */ - return Error::create( - \sprintf('Illegal character encoding [%s] specified.', $encoding) - ); - } - $converted = $encoding === null ? - @\mb_convert_encoding($input, 'UTF-8') : - @\mb_convert_encoding($input, 'UTF-8', $encoding); - /** - * this is for support UTF-8 with BOM encoding - * @see https://en.wikipedia.org/wiki/Byte_order_mark - * @see https://github.com/vlucas/phpdotenv/issues/500 - */ - if (\substr($converted, 0, 3) == "\xEF\xBB\xBF") { - $converted = \substr($converted, 3); - } - /** @var \GrahamCampbell\ResultType\Result */ - return Success::create($converted); - } - - /** - * Search for a given substring of the input. - * - * @param string $haystack - * @param string $needle - * - * @return \PhpOption\Option - */ - public static function pos(string $haystack, string $needle) - { - /** @var \PhpOption\Option */ - return Option::fromValue(\mb_strpos($haystack, $needle, 0, 'UTF-8'), false); - } - - /** - * Grab the specified substring of the input. - * - * @param string $input - * @param int $start - * @param int|null $length - * - * @return string - */ - public static function substr(string $input, int $start, int $length = null) - { - return \mb_substr($input, $start, $length, 'UTF-8'); - } - - /** - * Compute the length of the given string. - * - * @param string $input - * - * @return int - */ - public static function len(string $input) - { - return \mb_strlen($input, 'UTF-8'); - } -} diff --git a/src/Dotenv/Validator.php b/src/Dotenv/Validator.php deleted file mode 100644 index 0c04ab62..00000000 --- a/src/Dotenv/Validator.php +++ /dev/null @@ -1,209 +0,0 @@ -repository = $repository; - $this->variables = $variables; - } - - /** - * Assert that each variable is present. - * - * @throws \Dotenv\Exception\ValidationException - * - * @return \Dotenv\Validator - */ - public function required() - { - return $this->assert( - static function (?string $value) { - return $value !== null; - }, - 'is missing' - ); - } - - /** - * Assert that each variable is not empty. - * - * @throws \Dotenv\Exception\ValidationException - * - * @return \Dotenv\Validator - */ - public function notEmpty() - { - return $this->assertNullable( - static function (string $value) { - return Str::len(\trim($value)) > 0; - }, - 'is empty' - ); - } - - /** - * Assert that each specified variable is an integer. - * - * @throws \Dotenv\Exception\ValidationException - * - * @return \Dotenv\Validator - */ - public function isInteger() - { - return $this->assertNullable( - static function (string $value) { - return \ctype_digit($value); - }, - 'is not an integer' - ); - } - - /** - * Assert that each specified variable is a boolean. - * - * @throws \Dotenv\Exception\ValidationException - * - * @return \Dotenv\Validator - */ - public function isBoolean() - { - return $this->assertNullable( - static function (string $value) { - if ($value === '') { - return false; - } - - return \filter_var($value, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE) !== null; - }, - 'is not a boolean' - ); - } - - /** - * Assert that each variable is amongst the given choices. - * - * @param string[] $choices - * - * @throws \Dotenv\Exception\ValidationException - * - * @return \Dotenv\Validator - */ - public function allowedValues(array $choices) - { - return $this->assertNullable( - static function (string $value) use ($choices) { - return \in_array($value, $choices, true); - }, - \sprintf('is not one of [%s]', \implode(', ', $choices)) - ); - } - - /** - * Assert that each variable matches the given regular expression. - * - * @param string $regex - * - * @throws \Dotenv\Exception\ValidationException - * - * @return \Dotenv\Validator - */ - public function allowedRegexValues(string $regex) - { - return $this->assertNullable( - static function (string $value) use ($regex) { - return Regex::matches($regex, $value)->success()->getOrElse(false); - }, - \sprintf('does not match "%s"', $regex) - ); - } - - /** - * Assert that the callback returns true for each variable. - * - * @param callable(?string):bool $callback - * @param string $message - * - * @throws \Dotenv\Exception\ValidationException - * - * @return \Dotenv\Validator - */ - public function assert(callable $callback, string $message) - { - $failing = []; - - foreach ($this->variables as $variable) { - if ($callback($this->repository->get($variable)) === false) { - $failing[] = \sprintf('%s %s', $variable, $message); - } - } - - if (\count($failing) > 0) { - throw new ValidationException(\sprintf( - 'One or more environment variables failed assertions: %s.', - \implode(', ', $failing) - )); - } - - return $this; - } - - /** - * Assert that the callback returns true for each variable. - * - * Skip checking null variable values. - * - * @param callable(string):bool $callback - * @param string $message - * - * @throws \Dotenv\Exception\ValidationException - * - * @return \Dotenv\Validator - */ - public function assertNullable(callable $callback, string $message) - { - return $this->assert( - static function (?string $value) use ($callback) { - if ($value === null) { - return true; - } - - return $callback($value); - }, - $message - ); - } -} diff --git a/src/GrahamCampbell/ResultType/Error.php b/src/GrahamCampbell/ResultType/Error.php deleted file mode 100644 index 2c37c3e2..00000000 --- a/src/GrahamCampbell/ResultType/Error.php +++ /dev/null @@ -1,121 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace GrahamCampbell\ResultType; - -use PhpOption\None; -use PhpOption\Some; - -/** - * @template T - * @template E - * - * @extends \GrahamCampbell\ResultType\Result - */ -final class Error extends Result -{ - /** - * @var E - */ - private $value; - - /** - * Internal constructor for an error value. - * - * @param E $value - * - * @return void - */ - private function __construct($value) - { - $this->value = $value; - } - - /** - * Create a new error value. - * - * @template F - * - * @param F $value - * - * @return \GrahamCampbell\ResultType\Result - */ - public static function create($value) - { - return new self($value); - } - - /** - * Get the success option value. - * - * @return \PhpOption\Option - */ - public function success() - { - return None::create(); - } - - /** - * Map over the success value. - * - * @template S - * - * @param callable(T):S $f - * - * @return \GrahamCampbell\ResultType\Result - */ - public function map(callable $f) - { - return self::create($this->value); - } - - /** - * Flat map over the success value. - * - * @template S - * @template F - * - * @param callable(T):\GrahamCampbell\ResultType\Result $f - * - * @return \GrahamCampbell\ResultType\Result - */ - public function flatMap(callable $f) - { - /** @var \GrahamCampbell\ResultType\Result */ - return self::create($this->value); - } - - /** - * Get the error option value. - * - * @return \PhpOption\Option - */ - public function error() - { - return Some::create($this->value); - } - - /** - * Map over the error value. - * - * @template F - * - * @param callable(E):F $f - * - * @return \GrahamCampbell\ResultType\Result - */ - public function mapError(callable $f) - { - return self::create($f($this->value)); - } -} diff --git a/src/GrahamCampbell/ResultType/Result.php b/src/GrahamCampbell/ResultType/Result.php deleted file mode 100644 index 8c67bcdd..00000000 --- a/src/GrahamCampbell/ResultType/Result.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace GrahamCampbell\ResultType; - -/** - * @template T - * @template E - */ -abstract class Result -{ - /** - * Get the success option value. - * - * @return \PhpOption\Option - */ - abstract public function success(); - - /** - * Map over the success value. - * - * @template S - * - * @param callable(T):S $f - * - * @return \GrahamCampbell\ResultType\Result - */ - abstract public function map(callable $f); - - /** - * Flat map over the success value. - * - * @template S - * @template F - * - * @param callable(T):\GrahamCampbell\ResultType\Result $f - * - * @return \GrahamCampbell\ResultType\Result - */ - abstract public function flatMap(callable $f); - - /** - * Get the error option value. - * - * @return \PhpOption\Option - */ - abstract public function error(); - - /** - * Map over the error value. - * - * @template F - * - * @param callable(E):F $f - * - * @return \GrahamCampbell\ResultType\Result - */ - abstract public function mapError(callable $f); -} diff --git a/src/GrahamCampbell/ResultType/Success.php b/src/GrahamCampbell/ResultType/Success.php deleted file mode 100644 index 27cd85ee..00000000 --- a/src/GrahamCampbell/ResultType/Success.php +++ /dev/null @@ -1,120 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace GrahamCampbell\ResultType; - -use PhpOption\None; -use PhpOption\Some; - -/** - * @template T - * @template E - * - * @extends \GrahamCampbell\ResultType\Result - */ -final class Success extends Result -{ - /** - * @var T - */ - private $value; - - /** - * Internal constructor for a success value. - * - * @param T $value - * - * @return void - */ - private function __construct($value) - { - $this->value = $value; - } - - /** - * Create a new error value. - * - * @template S - * - * @param S $value - * - * @return \GrahamCampbell\ResultType\Result - */ - public static function create($value) - { - return new self($value); - } - - /** - * Get the success option value. - * - * @return \PhpOption\Option - */ - public function success() - { - return Some::create($this->value); - } - - /** - * Map over the success value. - * - * @template S - * - * @param callable(T):S $f - * - * @return \GrahamCampbell\ResultType\Result - */ - public function map(callable $f) - { - return self::create($f($this->value)); - } - - /** - * Flat map over the success value. - * - * @template S - * @template F - * - * @param callable(T):\GrahamCampbell\ResultType\Result $f - * - * @return \GrahamCampbell\ResultType\Result - */ - public function flatMap(callable $f) - { - return $f($this->value); - } - - /** - * Get the error option value. - * - * @return \PhpOption\Option - */ - public function error() - { - return None::create(); - } - - /** - * Map over the error value. - * - * @template F - * - * @param callable(E):F $f - * - * @return \GrahamCampbell\ResultType\Result - */ - public function mapError(callable $f) - { - return self::create($this->value); - } -} diff --git a/src/PhpOption/LazyOption.php b/src/PhpOption/LazyOption.php deleted file mode 100644 index 9cb77c86..00000000 --- a/src/PhpOption/LazyOption.php +++ /dev/null @@ -1,175 +0,0 @@ - - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -namespace PhpOption; - -use Traversable; - -/** - * @template T - * - * @extends Option - */ -final class LazyOption extends Option -{ - /** @var callable(mixed...):(Option) */ - private $callback; - - /** @var array */ - private $arguments; - - /** @var Option|null */ - private $option; - - /** - * @template S - * @param callable(mixed...):(Option) $callback - * @param array $arguments - * - * @return LazyOption - */ - public static function create($callback, array $arguments = []): self - { - return new self($callback, $arguments); - } - - /** - * @param callable(mixed...):(Option) $callback - * @param array $arguments - */ - public function __construct($callback, array $arguments = []) - { - if (!is_callable($callback)) { - throw new \InvalidArgumentException('Invalid callback given'); - } - - $this->callback = $callback; - $this->arguments = $arguments; - } - - public function isDefined(): bool - { - return $this->option()->isDefined(); - } - - public function isEmpty(): bool - { - return $this->option()->isEmpty(); - } - - public function get() - { - return $this->option()->get(); - } - - public function getOrElse($default) - { - return $this->option()->getOrElse($default); - } - - public function getOrCall($callable) - { - return $this->option()->getOrCall($callable); - } - - public function getOrThrow(\Exception $ex) - { - return $this->option()->getOrThrow($ex); - } - - public function orElse(Option $else) - { - return $this->option()->orElse($else); - } - - public function ifDefined($callable) - { - $this->option()->forAll($callable); - } - - public function forAll($callable) - { - return $this->option()->forAll($callable); - } - - public function map($callable) - { - return $this->option()->map($callable); - } - - public function flatMap($callable) - { - return $this->option()->flatMap($callable); - } - - public function filter($callable) - { - return $this->option()->filter($callable); - } - - public function filterNot($callable) - { - return $this->option()->filterNot($callable); - } - - public function select($value) - { - return $this->option()->select($value); - } - - public function reject($value) - { - return $this->option()->reject($value); - } - - /** - * @return Traversable - */ - public function getIterator(): Traversable - { - return $this->option()->getIterator(); - } - - public function foldLeft($initialValue, $callable) - { - return $this->option()->foldLeft($initialValue, $callable); - } - - public function foldRight($initialValue, $callable) - { - return $this->option()->foldRight($initialValue, $callable); - } - - /** - * @return Option - */ - private function option(): Option - { - if (null === $this->option) { - /** @var mixed */ - $option = call_user_func_array($this->callback, $this->arguments); - if ($option instanceof Option) { - $this->option = $option; - } else { - throw new \RuntimeException(sprintf('Expected instance of %s', Option::class)); - } - } - - return $this->option; - } -} diff --git a/src/PhpOption/None.php b/src/PhpOption/None.php deleted file mode 100644 index 4b85d22d..00000000 --- a/src/PhpOption/None.php +++ /dev/null @@ -1,136 +0,0 @@ - - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -namespace PhpOption; - -use EmptyIterator; - -/** - * @extends Option - */ -final class None extends Option -{ - /** @var None|null */ - private static $instance; - - /** - * @return None - */ - public static function create(): self - { - if (null === self::$instance) { - self::$instance = new self(); - } - - return self::$instance; - } - - public function get() - { - throw new \RuntimeException('None has no value.'); - } - - public function getOrCall($callable) - { - return $callable(); - } - - public function getOrElse($default) - { - return $default; - } - - public function getOrThrow(\Exception $ex) - { - throw $ex; - } - - public function isEmpty(): bool - { - return true; - } - - public function isDefined(): bool - { - return false; - } - - public function orElse(Option $else) - { - return $else; - } - - public function ifDefined($callable) - { - // Just do nothing in that case. - } - - public function forAll($callable) - { - return $this; - } - - public function map($callable) - { - return $this; - } - - public function flatMap($callable) - { - return $this; - } - - public function filter($callable) - { - return $this; - } - - public function filterNot($callable) - { - return $this; - } - - public function select($value) - { - return $this; - } - - public function reject($value) - { - return $this; - } - - public function getIterator(): EmptyIterator - { - return new EmptyIterator(); - } - - public function foldLeft($initialValue, $callable) - { - return $initialValue; - } - - public function foldRight($initialValue, $callable) - { - return $initialValue; - } - - private function __construct() - { - } -} diff --git a/src/PhpOption/Option.php b/src/PhpOption/Option.php deleted file mode 100644 index 172924cf..00000000 --- a/src/PhpOption/Option.php +++ /dev/null @@ -1,434 +0,0 @@ - - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -namespace PhpOption; - -use ArrayAccess; -use IteratorAggregate; - -/** - * @template T - * - * @implements IteratorAggregate - */ -abstract class Option implements IteratorAggregate -{ - /** - * Creates an option given a return value. - * - * This is intended for consuming existing APIs and allows you to easily - * convert them to an option. By default, we treat ``null`` as the None - * case, and everything else as Some. - * - * @template S - * - * @param S $value The actual return value. - * @param S $noneValue The value which should be considered "None"; null by - * default. - * - * @return Option - */ - public static function fromValue($value, $noneValue = null) - { - if ($value === $noneValue) { - return None::create(); - } - - return new Some($value); - } - - /** - * Creates an option from an array's value. - * - * If the key does not exist in the array, the array is not actually an - * array, or the array's value at the given key is null, None is returned. - * Otherwise, Some is returned wrapping the value at the given key. - * - * @template S - * - * @param array|ArrayAccess|null $array A potential array or \ArrayAccess value. - * @param string $key The key to check. - * - * @return Option - */ - public static function fromArraysValue($array, $key) - { - if (!(is_array($array) || $array instanceof ArrayAccess) || !isset($array[$key])) { - return None::create(); - } - - return new Some($array[$key]); - } - - /** - * Creates a lazy-option with the given callback. - * - * This is also a helper constructor for lazy-consuming existing APIs where - * the return value is not yet an option. By default, we treat ``null`` as - * None case, and everything else as Some. - * - * @template S - * - * @param callable $callback The callback to evaluate. - * @param array $arguments The arguments for the callback. - * @param S $noneValue The value which should be considered "None"; - * null by default. - * - * @return LazyOption - */ - public static function fromReturn($callback, array $arguments = [], $noneValue = null) - { - return new LazyOption(static function () use ($callback, $arguments, $noneValue) { - /** @var mixed */ - $return = call_user_func_array($callback, $arguments); - - if ($return === $noneValue) { - return None::create(); - } - - return new Some($return); - }); - } - - /** - * Option factory, which creates new option based on passed value. - * - * If value is already an option, it simply returns. If value is callable, - * LazyOption with passed callback created and returned. If Option - * returned from callback, it returns directly. On other case value passed - * to Option::fromValue() method. - * - * @template S - * - * @param Option|callable|S $value - * @param S $noneValue Used when $value is mixed or - * callable, for None-check. - * - * @return Option|LazyOption - */ - public static function ensure($value, $noneValue = null) - { - if ($value instanceof self) { - return $value; - } elseif (is_callable($value)) { - return new LazyOption(static function () use ($value, $noneValue) { - /** @var mixed */ - $return = $value(); - - if ($return instanceof self) { - return $return; - } else { - return self::fromValue($return, $noneValue); - } - }); - } else { - return self::fromValue($value, $noneValue); - } - } - - /** - * Lift a function so that it accepts Option as parameters. - * - * We return a new closure that wraps the original callback. If any of the - * parameters passed to the lifted function is empty, the function will - * return a value of None. Otherwise, we will pass all parameters to the - * original callback and return the value inside a new Option, unless an - * Option is returned from the function, in which case, we use that. - * - * @template S - * - * @param callable $callback - * @param mixed $noneValue - * - * @return callable - */ - public static function lift($callback, $noneValue = null) - { - return static function () use ($callback, $noneValue) { - /** @var array */ - $args = func_get_args(); - - $reduced_args = array_reduce( - $args, - /** @param bool $status */ - static function ($status, self $o) { - return $o->isEmpty() ? true : $status; - }, - false - ); - // if at least one parameter is empty, return None - if ($reduced_args) { - return None::create(); - } - - $args = array_map( - /** @return T */ - static function (self $o) { - // it is safe to do so because the fold above checked - // that all arguments are of type Some - /** @var T */ - return $o->get(); - }, - $args - ); - - return self::ensure(call_user_func_array($callback, $args), $noneValue); - }; - } - - /** - * Returns the value if available, or throws an exception otherwise. - * - * @throws \RuntimeException If value is not available. - * - * @return T - */ - abstract public function get(); - - /** - * Returns the value if available, or the default value if not. - * - * @template S - * - * @param S $default - * - * @return T|S - */ - abstract public function getOrElse($default); - - /** - * Returns the value if available, or the results of the callable. - * - * This is preferable over ``getOrElse`` if the computation of the default - * value is expensive. - * - * @template S - * - * @param callable():S $callable - * - * @return T|S - */ - abstract public function getOrCall($callable); - - /** - * Returns the value if available, or throws the passed exception. - * - * @param \Exception $ex - * - * @return T - */ - abstract public function getOrThrow(\Exception $ex); - - /** - * Returns true if no value is available, false otherwise. - * - * @return bool - */ - abstract public function isEmpty(); - - /** - * Returns true if a value is available, false otherwise. - * - * @return bool - */ - abstract public function isDefined(); - - /** - * Returns this option if non-empty, or the passed option otherwise. - * - * This can be used to try multiple alternatives, and is especially useful - * with lazy evaluating options: - * - * ```php - * $repo->findSomething() - * ->orElse(new LazyOption(array($repo, 'findSomethingElse'))) - * ->orElse(new LazyOption(array($repo, 'createSomething'))); - * ``` - * - * @param Option $else - * - * @return Option - */ - abstract public function orElse(self $else); - - /** - * This is similar to map() below except that the return value has no meaning; - * the passed callable is simply executed if the option is non-empty, and - * ignored if the option is empty. - * - * In all cases, the return value of the callable is discarded. - * - * ```php - * $comment->getMaybeFile()->ifDefined(function($file) { - * // Do something with $file here. - * }); - * ``` - * - * If you're looking for something like ``ifEmpty``, you can use ``getOrCall`` - * and ``getOrElse`` in these cases. - * - * @deprecated Use forAll() instead. - * - * @param callable(T):mixed $callable - * - * @return void - */ - abstract public function ifDefined($callable); - - /** - * This is similar to map() except that the return value of the callable has no meaning. - * - * The passed callable is simply executed if the option is non-empty, and ignored if the - * option is empty. This method is preferred for callables with side-effects, while map() - * is intended for callables without side-effects. - * - * @param callable(T):mixed $callable - * - * @return Option - */ - abstract public function forAll($callable); - - /** - * Applies the callable to the value of the option if it is non-empty, - * and returns the return value of the callable wrapped in Some(). - * - * If the option is empty, then the callable is not applied. - * - * ```php - * (new Some("foo"))->map('strtoupper')->get(); // "FOO" - * ``` - * - * @template S - * - * @param callable(T):S $callable - * - * @return Option - */ - abstract public function map($callable); - - /** - * Applies the callable to the value of the option if it is non-empty, and - * returns the return value of the callable directly. - * - * In contrast to ``map``, the return value of the callable is expected to - * be an Option itself; it is not automatically wrapped in Some(). - * - * @template S - * - * @param callable(T):Option $callable must return an Option - * - * @return Option - */ - abstract public function flatMap($callable); - - /** - * If the option is empty, it is returned immediately without applying the callable. - * - * If the option is non-empty, the callable is applied, and if it returns true, - * the option itself is returned; otherwise, None is returned. - * - * @param callable(T):bool $callable - * - * @return Option - */ - abstract public function filter($callable); - - /** - * If the option is empty, it is returned immediately without applying the callable. - * - * If the option is non-empty, the callable is applied, and if it returns false, - * the option itself is returned; otherwise, None is returned. - * - * @param callable(T):bool $callable - * - * @return Option - */ - abstract public function filterNot($callable); - - /** - * If the option is empty, it is returned immediately. - * - * If the option is non-empty, and its value does not equal the passed value - * (via a shallow comparison ===), then None is returned. Otherwise, the - * Option is returned. - * - * In other words, this will filter all but the passed value. - * - * @param T $value - * - * @return Option - */ - abstract public function select($value); - - /** - * If the option is empty, it is returned immediately. - * - * If the option is non-empty, and its value does equal the passed value (via - * a shallow comparison ===), then None is returned; otherwise, the Option is - * returned. - * - * In other words, this will let all values through except the passed value. - * - * @param T $value - * - * @return Option - */ - abstract public function reject($value); - - /** - * Binary operator for the initial value and the option's value. - * - * If empty, the initial value is returned. If non-empty, the callable - * receives the initial value and the option's value as arguments. - * - * ```php - * - * $some = new Some(5); - * $none = None::create(); - * $result = $some->foldLeft(1, function($a, $b) { return $a + $b; }); // int(6) - * $result = $none->foldLeft(1, function($a, $b) { return $a + $b; }); // int(1) - * - * // This can be used instead of something like the following: - * $option = Option::fromValue($integerOrNull); - * $result = 1; - * if ( ! $option->isEmpty()) { - * $result += $option->get(); - * } - * ``` - * - * @template S - * - * @param S $initialValue - * @param callable(S, T):S $callable - * - * @return S - */ - abstract public function foldLeft($initialValue, $callable); - - /** - * foldLeft() but with reversed arguments for the callable. - * - * @template S - * - * @param S $initialValue - * @param callable(T, S):S $callable - * - * @return S - */ - abstract public function foldRight($initialValue, $callable); -} diff --git a/src/PhpOption/Some.php b/src/PhpOption/Some.php deleted file mode 100644 index 032632ea..00000000 --- a/src/PhpOption/Some.php +++ /dev/null @@ -1,169 +0,0 @@ - - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -namespace PhpOption; - -use ArrayIterator; - -/** - * @template T - * - * @extends Option - */ -final class Some extends Option -{ - /** @var T */ - private $value; - - /** - * @param T $value - */ - public function __construct($value) - { - $this->value = $value; - } - - /** - * @template U - * - * @param U $value - * - * @return Some - */ - public static function create($value): self - { - return new self($value); - } - - public function isDefined(): bool - { - return true; - } - - public function isEmpty(): bool - { - return false; - } - - public function get() - { - return $this->value; - } - - public function getOrElse($default) - { - return $this->value; - } - - public function getOrCall($callable) - { - return $this->value; - } - - public function getOrThrow(\Exception $ex) - { - return $this->value; - } - - public function orElse(Option $else) - { - return $this; - } - - public function ifDefined($callable) - { - $this->forAll($callable); - } - - public function forAll($callable) - { - $callable($this->value); - - return $this; - } - - public function map($callable) - { - return new self($callable($this->value)); - } - - public function flatMap($callable) - { - /** @var mixed */ - $rs = $callable($this->value); - if (!$rs instanceof Option) { - throw new \RuntimeException('Callables passed to flatMap() must return an Option. Maybe you should use map() instead?'); - } - - return $rs; - } - - public function filter($callable) - { - if (true === $callable($this->value)) { - return $this; - } - - return None::create(); - } - - public function filterNot($callable) - { - if (false === $callable($this->value)) { - return $this; - } - - return None::create(); - } - - public function select($value) - { - if ($this->value === $value) { - return $this; - } - - return None::create(); - } - - public function reject($value) - { - if ($this->value === $value) { - return None::create(); - } - - return $this; - } - - /** - * @return ArrayIterator - */ - public function getIterator(): ArrayIterator - { - return new ArrayIterator([$this->value]); - } - - public function foldLeft($initialValue, $callable) - { - return $callable($initialValue, $this->value); - } - - public function foldRight($initialValue, $callable) - { - return $callable($this->value, $initialValue); - } -} From 0b9cbee2103315e11fcde163bb9ad278d646a685 Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 28 Feb 2024 19:49:46 +0100 Subject: [PATCH 24/51] Processed w/ phpcbf --- src/RaspAP/DotEnv/DotEnv.php | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/RaspAP/DotEnv/DotEnv.php b/src/RaspAP/DotEnv/DotEnv.php index edf3d906..a29a4e99 100644 --- a/src/RaspAP/DotEnv/DotEnv.php +++ b/src/RaspAP/DotEnv/DotEnv.php @@ -12,15 +12,18 @@ declare(strict_types=1); namespace RaspAP\DotEnv; -class DotEnv { +class DotEnv +{ protected $envFile; protected $data = []; - public function __construct($envFile = '.env') { + public function __construct($envFile = '.env') + { $this->envFile = $envFile; } - public function load() { + public function load() + { if (file_exists($this->envFile)) { $this->data = parse_ini_file($this->envFile); foreach ($this->data as $key => $value) { @@ -34,26 +37,31 @@ class DotEnv { } } - public function set($key, $value) { + public function set($key, $value) + { $this->data[$key] = $value; putenv("$key=$value"); $this->store($key, $value); } - public function get($key) { + public function get($key) + { return getenv($key); } - public function getAll() { + public function getAll() + { return $this->data; } - public function unset($key) { + public function unset($key) + { unset($_ENV[$key]); return $this; } - private function store($key, $value) { + private function store($key, $value) + { $content = file_get_contents($this->envFile); $content = preg_replace("/^$key=.*/m", "$key=$value", $content, 1, $count); if ($count === 0) { From 2b2cb8fa40c20480da6692f76bdab98853daeb87 Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 28 Feb 2024 19:50:20 +0100 Subject: [PATCH 25/51] Update en_US locale messages + compile .mo --- locale/en_US/LC_MESSAGES/messages.mo | Bin 41677 -> 42175 bytes locale/en_US/LC_MESSAGES/messages.po | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/locale/en_US/LC_MESSAGES/messages.mo b/locale/en_US/LC_MESSAGES/messages.mo index 10bd1cf2834f82e2afe14cbd76cdd91832eed6a3..eb29660e28f67632d35ec1051347763aa286d6b6 100644 GIT binary patch delta 11116 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 diff --git a/locale/en_US/LC_MESSAGES/messages.po b/locale/en_US/LC_MESSAGES/messages.po index 40058366..85e1195c 100644 --- a/locale/en_US/LC_MESSAGES/messages.po +++ b/locale/en_US/LC_MESSAGES/messages.po @@ -1528,3 +1528,26 @@ msgstr "RaspAP Exception" msgid "An exception occurred" msgstr "An exception occurred" +#: includes/restapi.php + +msgid "RestAPI" +msgstr "RestAPI" + +msgid "RestAPI settings" +msgstr "RestAPI settings" + +msgid "Start RestAPI service" +msgstr "Start RestAPI service" + +msgid "Stop RestAPI service" +msgstr "Stop RestAPI service" + +msgid "Saving API key" +msgstr "Saving API key" + +msgid "RestAPI status" +msgstr "RestAPI status" + +msgid "Current raspap-restapi.service status is displayed below." +msgstr "Current raspap-restapi.service status is displayed below." + From 5b7b96867621874b06ec2e2100b5b6ba45e6d6ae Mon Sep 17 00:00:00 2001 From: billz Date: Thu, 7 Mar 2024 18:44:27 +0100 Subject: [PATCH 26/51] Fix: rename systemd service, set serviceStatus flag --- includes/restapi.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/restapi.php b/includes/restapi.php index 015c0cb1..7ae0db6f 100644 --- a/includes/restapi.php +++ b/includes/restapi.php @@ -29,21 +29,21 @@ function DisplayRestAPI() } } } elseif (isset($_POST['StartRestAPIservice'])) { - $status->addMessage('Attempting to start raspap-restapi.service', 'info'); - exec('sudo /bin/systemctl start raspap-restapi', $return); + $status->addMessage('Attempting to start restapi.service', 'info'); + exec('sudo /bin/systemctl start restapi.service', $return); foreach ($return as $line) { $status->addMessage($line, 'info'); } } elseif (isset($_POST['StopRestAPIservice'])) { - $status->addMessage('Attempting to stop raspap-restapi.service', 'info'); - exec('sudo /bin/systemctl stop raspap-restapi.service', $return); + $status->addMessage('Attempting to stop restapi.service', 'info'); + exec('sudo /bin/systemctl stop restapi.service', $return); foreach ($return as $line) { $status->addMessage($line, 'info'); } } } - exec('pidof uvicorn | wc -l', $uvicorn); - $serviceStatus = $uvicorn[0] == 0 ? "down" : "up"; + exec("ps aux | grep -v grep | grep uvicorn", $output, $return); + $serviceStatus = !empty($output) ? "up" : "down"; echo renderTemplate("restapi", compact( "status", From 95acd497dfbb182c4597165297d4491bb76cfb87 Mon Sep 17 00:00:00 2001 From: billz Date: Thu, 7 Mar 2024 18:50:39 +0100 Subject: [PATCH 27/51] Move /api dir to $raspap_dir --- installers/common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installers/common.sh b/installers/common.sh index 2d05213d..bec35c32 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -584,7 +584,7 @@ function _create_openvpn_scripts() { # Install and enable RestAPI configuration option function _install_restapi() { _install_log "Installing and enabling RestAPI" - sudo cp -r "$webroot_dir/api" "$raspap_dir/api" || _install_status 1 "Unable to move api folder" + sudo mv -r "$webroot_dir/api" "$raspap_dir/api" || _install_status 1 "Unable to move api folder" if ! command -v python3 &> /dev/null; then echo "Python is not installed. Installing Python..." From edc3a421cabeedf9e97153817bcace28cd876d28 Mon Sep 17 00:00:00 2001 From: billz Date: Thu, 7 Mar 2024 18:51:16 +0100 Subject: [PATCH 28/51] Replace root user with %i, append --reload --- installers/restapi.service | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/installers/restapi.service b/installers/restapi.service index 63748422..237dce79 100644 --- a/installers/restapi.service +++ b/installers/restapi.service @@ -3,12 +3,13 @@ Description=raspap-restapi After=network.target [Service] -User=root +User=%i WorkingDirectory=/etc/raspap/api LimitNOFILE=4096 -ExecStart=/usr/bin/python3 -m uvicorn main:app --host 0.0.0.0 --port 8081 +ExecStart=/usr/bin/python3 -m uvicorn main:app --host 0.0.0.0 --port 8081 --reload Restart=on-failure RestartSec=5s [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target + From 2365c4e251375c90642eae7a7e5ce06e0964f455 Mon Sep 17 00:00:00 2001 From: billz Date: Thu, 7 Mar 2024 18:52:01 +0100 Subject: [PATCH 29/51] Add python-dotenv dependecy --- api/requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/requirements.txt b/api/requirements.txt index d0b660e0..7c82a713 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -1,3 +1,5 @@ fastapi==0.109.0 uvicorn==0.25.0 -psutil==5.9.8 \ No newline at end of file +psutil==5.9.8 +python-dotenv==1.0.1 + From 75be1bf04e3a772e28f313857f0cc6f216d585a2 Mon Sep 17 00:00:00 2001 From: billz Date: Thu, 7 Mar 2024 18:53:17 +0100 Subject: [PATCH 30/51] Add load_dotenv from dotenv --- api/auth.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/auth.py b/api/auth.py index 2a716e2d..bef3c6ae 100644 --- a/api/auth.py +++ b/api/auth.py @@ -2,6 +2,9 @@ import os from fastapi.security.api_key import APIKeyHeader from fastapi import Security, HTTPException from starlette.status import HTTP_403_FORBIDDEN +from dotenv import load_dotenv + +load_dotenv() apikey=os.getenv('RASPAP_API_KEY') #if env not set, set the api key to "insecure" @@ -17,4 +20,5 @@ async def get_api_key(api_key_header: str = Security(api_key_header)): else: raise HTTPException( status_code=HTTP_403_FORBIDDEN, detail="403: Unauthorized" - ) \ No newline at end of file + ) + From b80151be28b0b68e267b3321a5b14da4d6857a7b Mon Sep 17 00:00:00 2001 From: billz Date: Thu, 7 Mar 2024 18:54:15 +0100 Subject: [PATCH 31/51] Fix SyntaxError: positional argument follows keyword --- api/main.py | 60 ++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/api/main.py b/api/main.py index be87903e..edd02a42 100644 --- a/api/main.py +++ b/api/main.py @@ -28,8 +28,8 @@ app = FastAPI( } ) -@app.get("/system", tags=["system"], api_key: APIKey = Depends(auth.get_api_key) -async def get_system(): +@app.get("/system", tags=["system"]) +async def get_system(api_key: APIKey = Depends(auth.get_api_key)): return{ 'hostname': system.hostname(), 'uptime': system.uptime(), @@ -45,8 +45,8 @@ async def get_system(): 'rpiRevision': system.rpiRevision() } -@app.get("/ap", tags=["accesspoint/hostpost"], api_key: APIKey = Depends(auth.get_api_key) -async def get_ap(): +@app.get("/ap", tags=["accesspoint/hostpost"]) +async def get_ap(api_key: APIKey = Depends(auth.get_api_key)): return{ 'driver': ap.driver(), 'ctrl_interface': ap.ctrl_interface(), @@ -66,15 +66,15 @@ async def get_ap(): 'ignore_broadcast_ssid': ap.ignore_broadcast_ssid() } -@app.get("/clients/{wireless_interface}", tags=["Clients"], api_key: APIKey = Depends(auth.get_api_key) -async def get_clients(wireless_interface): +@app.get("/clients/{wireless_interface}", tags=["Clients"]) +async def get_clients(wireless_interface, api_key: APIKey = Depends(auth.get_api_key)): return{ 'active_clients_amount': client.get_active_clients_amount(wireless_interface), 'active_clients': json.loads(client.get_active_clients(wireless_interface)) } -@app.get("/dhcp", tags=["DHCP"], api_key: APIKey = Depends(auth.get_api_key) -async def get_dhcp(): +@app.get("/dhcp", tags=["DHCP"]) +async def get_dhcp(api_key: APIKey = Depends(auth.get_api_key)): return{ 'range_start': dhcp.range_start(), 'range_end': dhcp.range_end(), @@ -84,31 +84,31 @@ async def get_dhcp(): 'range_nameservers': dhcp.range_nameservers() } -@app.get("/dns/domains", tags=["DNS"], api_key: APIKey = Depends(auth.get_api_key) -async def get_domains(): +@app.get("/dns/domains", tags=["DNS"]) +async def get_domains(api_key: APIKey = Depends(auth.get_api_key)): return{ 'domains': json.loads(dns.adblockdomains()) } -@app.get("/dns/hostnames", tags=["DNS"], api_key: APIKey = Depends(auth.get_api_key) -async def get_hostnames(): +@app.get("/dns/hostnames", tags=["DNS"]) +async def get_hostnames(api_key: APIKey = Depends(auth.get_api_key)): return{ 'hostnames': json.loads(dns.adblockhostnames()) } -@app.get("/dns/upstream", tags=["DNS"], api_key: APIKey = Depends(auth.get_api_key) -async def get_upstream(): +@app.get("/dns/upstream", tags=["DNS"]) +async def get_upstream(api_key: APIKey = Depends(auth.get_api_key)): return{ 'upstream_nameserver': dns.upstream_nameserver() } -@app.get("/dns/logs", tags=["DNS"], api_key: APIKey = Depends(auth.get_api_key) -async def get_dnsmasq_logs(): +@app.get("/dns/logs", tags=["DNS"]) +async def get_dnsmasq_logs(api_key: APIKey = Depends(auth.get_api_key)): return(dns.dnsmasq_logs()) -@app.get("/ddns", tags=["DDNS"], api_key: APIKey = Depends(auth.get_api_key) -async def get_ddns(): +@app.get("/ddns", tags=["DDNS"]) +async def get_ddns(api_key: APIKey = Depends(auth.get_api_key)): return{ 'use': ddns.use(), 'method': ddns.method(), @@ -119,19 +119,19 @@ async def get_ddns(): 'domain': ddns.domain() } -@app.get("/firewall", tags=["Firewall"], api_key: APIKey = Depends(auth.get_api_key) -async def get_firewall(): +@app.get("/firewall", tags=["Firewall"]) +async def get_firewall(api_key: APIKey = Depends(auth.get_api_key)): return json.loads(firewall.firewall_rules()) -@app.get("/networking", tags=["Networking"], api_key: APIKey = Depends(auth.get_api_key) -async def get_networking(): +@app.get("/networking", tags=["Networking"]) +async def get_networking(api_key: APIKey = Depends(auth.get_api_key)): return{ 'interfaces': json.loads(networking.interfaces()), 'throughput': json.loads(networking.throughput()) } -@app.get("/openvpn", tags=["OpenVPN"], api_key: APIKey = Depends(auth.get_api_key) -async def get_openvpn(): +@app.get("/openvpn", tags=["OpenVPN"]) +async def get_openvpn(api_key: APIKey = Depends(auth.get_api_key)): return{ 'client_configs': openvpn.client_configs(), 'client_config_names': openvpn.client_config_names(), @@ -140,22 +140,22 @@ async def get_openvpn(): 'client_login_active': openvpn.client_login_active() } -@app.get("/openvpn/{config}", tags=["OpenVPN"], api_key: APIKey = Depends(auth.get_api_key) -async def client_config_list(config): +@app.get("/openvpn/{config}", tags=["OpenVPN"]) +async def client_config_list(config, api_key: APIKey = Depends(auth.get_api_key)): return{ 'client_config': openvpn.client_config_list(config) } -@app.get("/wireguard", tags=["WireGuard"], api_key: APIKey = Depends(auth.get_api_key) -async def get_wireguard(): +@app.get("/wireguard", tags=["WireGuard"]) +async def get_wireguard(api_key: APIKey = Depends(auth.get_api_key)): return{ 'client_configs': wireguard.configs(), 'client_config_names': wireguard.client_config_names(), 'client_config_active': wireguard.client_config_active() } -@app.get("/wireguard/{config}", tags=["WireGuard"], api_key: APIKey = Depends(auth.get_api_key) -async def client_config_list(config): +@app.get("/wireguard/{config}", tags=["WireGuard"]) +async def client_config_list(config, api_key: APIKey = Depends(auth.get_api_key)): return{ 'client_config': wireguard.client_config_list(config) } From 00f90f1f732b1698272f5cc167977fc62e8feebb Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 08:59:36 +0100 Subject: [PATCH 32/51] Sanitize user-provided inputs --- api/modules/client.py | 44 ++++++++++++++++++++++++---------------- api/modules/openvpn.py | 4 ++-- api/modules/wireguard.py | 11 +++++++--- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/api/modules/client.py b/api/modules/client.py index 2e894cd5..63caf40d 100644 --- a/api/modules/client.py +++ b/api/modules/client.py @@ -2,27 +2,37 @@ import subprocess import json def get_active_clients_amount(interface): - output = subprocess.run(f'''cat '/var/lib/misc/dnsmasq.leases' | grep -iwE "$(arp -i '{interface}' | grep -oE "(([0-9]|[a-f]|[A-F]){{{2}}}:){{{5}}}([0-9]|[a-f]|[A-F]){{{2}}}")"''', shell=True, capture_output=True, text=True) - return(len(output.stdout.splitlines())) + arp_output = subprocess.run(['arp', '-i', interface], capture_output=True, text=True) + mac_addresses = arp_output.stdout.splitlines() + + if mac_addresses: + grep_pattern = '|'.join(mac_addresses) + output = subprocess.run(['grep', '-iwE', grep_pattern, '/var/lib/misc/dnsmasq.leases'], capture_output=True, text=True) + return len(output.stdout.splitlines()) + else: + return 0 def get_active_clients(interface): - #does not run like intended, but it works.... - output = subprocess.run(f'''cat '/var/lib/misc/dnsmasq.leases' | grep -iwE "$(arp -i '{interface}' | grep -oE "(([0-9]|[a-f]|[A-F]){{{2}}}:){{{5}}}([0-9]|[a-f]|[A-F]){{{2}}}")"''', shell=True, capture_output=True, text=True) - clients_list = [] + arp_output = subprocess.run(['arp', '-i', interface], capture_output=True, text=True) + arp_mac_addresses = set(line.split()[2] for line in arp_output.stdout.splitlines()[1:]) - for line in output.stdout.splitlines(): + dnsmasq_output = subprocess.run(['cat', '/var/lib/misc/dnsmasq.leases'], capture_output=True, text=True) + active_clients = [] + + for line in dnsmasq_output.stdout.splitlines(): fields = line.split() + mac_address = fields[1] - client_data = { - "timestamp": int(fields[0]), - "mac_address": fields[1], - "ip_address": fields[2], - "hostname": fields[3], - "client_id": fields[4], - } + if mac_address in arp_mac_addresses: + client_data = { + "timestamp": int(fields[0]), + "mac_address": fields[1], + "ip_address": fields[2], + "hostname": fields[3], + "client_id": fields[4], + } + active_clients.append(client_data) - clients_list.append(client_data) + json_output = json.dumps(active_clients, indent=2) + return json_output - json_output = json.dumps(clients_list, indent=2) - - return json_output \ No newline at end of file diff --git a/api/modules/openvpn.py b/api/modules/openvpn.py index b8ad1f96..9000cfc1 100644 --- a/api/modules/openvpn.py +++ b/api/modules/openvpn.py @@ -34,8 +34,8 @@ def client_login_active(): return(active_config[1]) def client_config_list(client_config): - output = subprocess.run(f"cat /etc/openvpn/client/{client_config}", shell=True, capture_output=True, text=True).stdout.strip() + output = subprocess.run(["cat", f"/etc/openvpn/client/{client_config}"], capture_output=True, text=True).stdout.strip() return output.split('\n') #TODO: where is the logfile?? -#TODO: is service connected? \ No newline at end of file +#TODO: is service connected? diff --git a/api/modules/wireguard.py b/api/modules/wireguard.py index dd33970b..1ded47fa 100644 --- a/api/modules/wireguard.py +++ b/api/modules/wireguard.py @@ -19,8 +19,13 @@ def client_config_active(): return(active_config[1]) def client_config_list(client_config): - output = subprocess.run(f"cat /etc/wireguard/{client_config}", shell=True, capture_output=True, text=True).stdout.strip() - return output.split('\n') + config_path = f"/etc/wireguard/{client_config}" + try: + with open(config_path, 'r') as f: + output = f.read().strip() + return output.split('\n') + except FileNotFoundError: + raise FileNotFoundError("Client configuration file not found") #TODO: where is the logfile?? -#TODO: is service connected? \ No newline at end of file +#TODO: is service connected? From b567f565d9ac053138489d385bb41348f97afcb5 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 09:01:13 +0100 Subject: [PATCH 33/51] Disambiguate service name --- templates/restapi.php | 2 +- templates/restapi/status.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/restapi.php b/templates/restapi.php index 0014fc6d..2e76b4c7 100644 --- a/templates/restapi.php +++ b/templates/restapi.php @@ -20,7 +20,7 @@

diff --git a/templates/restapi/status.php b/templates/restapi/status.php index 4fc87c88..4cfacb15 100644 --- a/templates/restapi/status.php +++ b/templates/restapi/status.php @@ -1,7 +1,7 @@

-

raspap-restapi.service status is displayed below."); ?>

+

restapi.service status is displayed below."); ?>

From 95ad90063ba74866b248c547890c54120f849f55 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 09:08:53 +0100 Subject: [PATCH 34/51] Validate client_config path expression --- api/modules/wireguard.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/modules/wireguard.py b/api/modules/wireguard.py index 1ded47fa..904d87bb 100644 --- a/api/modules/wireguard.py +++ b/api/modules/wireguard.py @@ -1,4 +1,5 @@ import subprocess +import re def configs(): #ignore symlinks, because wg0.conf is in production the main config, but in insiders it is a symlink @@ -19,6 +20,10 @@ def client_config_active(): return(active_config[1]) def client_config_list(client_config): + pattern = r'^[a-zA-Z0-9_-]+$' + if not re.match(pattern, client_config): + raise ValueError("Invalid client_config") + config_path = f"/etc/wireguard/{client_config}" try: with open(config_path, 'r') as f: From 282b839f451ce03d7c2eced4a81c91ec9103b569 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 10:45:05 +0100 Subject: [PATCH 35/51] Add gen_apikey to template --- app/js/custom.js | 4 ++++ templates/restapi.php | 4 ++-- templates/restapi/general.php | 11 ++++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/js/custom.js b/app/js/custom.js index 8cbeeecf..febf7609 100644 --- a/app/js/custom.js +++ b/app/js/custom.js @@ -122,6 +122,10 @@ $(document).on("click", "#gen_wpa_passphrase", function(e) { $('#txtwpapassphrase').val(genPassword(63)); }); +$(document).on("click", "#gen_apikey", function(e) { + $('#txtapikey').val(genPassword(32).toLowerCase()); +}); + $(document).on("click", "#js-clearhostapd-log", function(e) { var csrfToken = $('meta[name=csrf_token]').attr('content'); $.post('ajax/logging/clearlog.php?',{'logfile':'/tmp/hostapd.log', 'csrf_token': csrfToken},function(data){ diff --git a/templates/restapi.php b/templates/restapi.php index 2e76b4c7..fc1d1437 100644 --- a/templates/restapi.php +++ b/templates/restapi.php @@ -2,9 +2,9 @@ " /> - class="btn btn-success " name="StartRestAPIservice" value="" /> + " /> - class="btn btn-warning " name="StopRestAPIservice" value="" /> + " /> diff --git a/templates/restapi/general.php b/templates/restapi/general.php index 41296084..b39c293c 100644 --- a/templates/restapi/general.php +++ b/templates/restapi/general.php @@ -5,9 +5,14 @@
- -
- +
+ +
+ +
+
+ +
From 5d8fed824a055554ebcba76499a179e7c4d63e70 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 10:46:45 +0100 Subject: [PATCH 36/51] Define RASPI_CONFIG_API, update class constructor, load + createEnv --- config/config.php | 1 + includes/defaults.php | 1 + src/RaspAP/DotEnv/DotEnv.php | 20 ++++++++++++++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/config/config.php b/config/config.php index e1d1d50d..2950632d 100755 --- a/config/config.php +++ b/config/config.php @@ -4,6 +4,7 @@ define('RASPI_BRAND_TEXT', 'RaspAP'); define('RASPI_CONFIG', '/etc/raspap'); define('RASPI_CONFIG_NETWORK', RASPI_CONFIG.'/networking/defaults.json'); define('RASPI_CONFIG_PROVIDERS', 'config/vpn-providers.json'); +define('RASPI_CONFIG_API', RASPI_CONFIG.'/api'); define('RASPI_ADMIN_DETAILS', RASPI_CONFIG.'/raspap.auth'); define('RASPI_WIFI_AP_INTERFACE', 'wlan0'); define('RASPI_CACHE_PATH', sys_get_temp_dir() . '/raspap'); diff --git a/includes/defaults.php b/includes/defaults.php index 7227a955..bc442e7d 100755 --- a/includes/defaults.php +++ b/includes/defaults.php @@ -9,6 +9,7 @@ $defaults = [ 'RASPI_VERSION' => '3.0.8', 'RASPI_CONFIG_NETWORK' => RASPI_CONFIG.'/networking/defaults.json', 'RASPI_CONFIG_PROVIDERS' => 'config/vpn-providers.json', + 'RASPI_CONFIG_API' => RASPI_CONFIG.'/api', 'RASPI_ADMIN_DETAILS' => RASPI_CONFIG.'/raspap.auth', 'RASPI_WIFI_AP_INTERFACE' => 'wlan0', 'RASPI_CACHE_PATH' => sys_get_temp_dir() . '/raspap', diff --git a/src/RaspAP/DotEnv/DotEnv.php b/src/RaspAP/DotEnv/DotEnv.php index a29a4e99..1e8397a1 100644 --- a/src/RaspAP/DotEnv/DotEnv.php +++ b/src/RaspAP/DotEnv/DotEnv.php @@ -17,13 +17,17 @@ class DotEnv protected $envFile; protected $data = []; - public function __construct($envFile = '.env') + public function __construct($envFile = RASPI_CONFIG_API. '/.env') { $this->envFile = $envFile; } public function load() { + if (!file_exists($this->envFile)) { + $this->createEnv(); + } + if (file_exists($this->envFile)) { $this->data = parse_ini_file($this->envFile); foreach ($this->data as $key => $value) { @@ -68,7 +72,19 @@ class DotEnv // if key doesn't exist, append it $content .= "$key=$value\n"; } - file_put_contents($this->envFile, $content); + file_put_contents("/tmp/.env", $content); + system('sudo mv /tmp/.env '.$this->envFile, $result); + if ($result !== 0) { + throw new Exception("Unable to move .env file: ". $this->envFile); + } + } + + protected function createEnv() + { + exec('sudo touch '. escapeshellarg($this->envFile), $output, $result); + if ($result !== 0) { + throw new Exception("Unable to create .env file: ". $this->envFile); + } } } From 87216bdc02b5cda5f5d884d7961e1c2d380bcb73 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 10:48:41 +0100 Subject: [PATCH 37/51] Update sudoers .env permissions, systemd service user --- installers/raspap.sudoers | 3 +++ installers/restapi.service | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index ed84ad00..b87fdb80 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -28,6 +28,9 @@ www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop openvpn-client@client www-data ALL=(ALL) NOPASSWD:/bin/systemctl disable openvpn-client@client www-data ALL=(ALL) NOPASSWD:/bin/systemctl start restapi.service www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop restapi.service +www-data ALL=(ALL) NOPASSWD:/bin/systemctl status restapi.service +www-data ALL=(ALL) NOPASSWD:/bin/touch /etc/raspap/api/.env +www-data ALL=(ALL) NOPASSWD:/bin/mv /tmp/.env /etc/raspap/api/.env www-data ALL=(ALL) NOPASSWD:/bin/mv /tmp/ovpn/* /etc/openvpn/client/*.conf www-data ALL=(ALL) NOPASSWD:/usr/bin/ln -s /etc/openvpn/client/*.conf /etc/openvpn/client/*.conf www-data ALL=(ALL) NOPASSWD:/bin/rm /etc/openvpn/client/*.conf diff --git a/installers/restapi.service b/installers/restapi.service index 237dce79..4fccd105 100644 --- a/installers/restapi.service +++ b/installers/restapi.service @@ -3,10 +3,11 @@ Description=raspap-restapi After=network.target [Service] -User=%i +User=pi WorkingDirectory=/etc/raspap/api LimitNOFILE=4096 ExecStart=/usr/bin/python3 -m uvicorn main:app --host 0.0.0.0 --port 8081 --reload +ExecStop=/bin/kill -HUP ${MAINPID} Restart=on-failure RestartSec=5s From ef7b67a4457cd53c5985b1bbdc79f856b2c52fbe Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 10:50:02 +0100 Subject: [PATCH 38/51] Set serviceLog from systemctl status --- includes/restapi.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/includes/restapi.php b/includes/restapi.php index 7ae0db6f..ff16c997 100644 --- a/includes/restapi.php +++ b/includes/restapi.php @@ -45,6 +45,9 @@ function DisplayRestAPI() exec("ps aux | grep -v grep | grep uvicorn", $output, $return); $serviceStatus = !empty($output) ? "up" : "down"; + exec("sudo systemctl status restapi.service", $output, $return); + $serviceLog = implode("\n", $output); + echo renderTemplate("restapi", compact( "status", "apiKey", From 2cdf6ef53e8c5ee2f48da652143fdfe5776d1663 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 11:15:31 +0100 Subject: [PATCH 39/51] Sanitize path to prevent directory traversal --- .gitignore | 1 - api/modules/wireguard.py | 16 ++++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 245d6fa4..a2a77b49 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ yarn-error.log includes/config.php rootCA.pem vendor -.env diff --git a/api/modules/wireguard.py b/api/modules/wireguard.py index 904d87bb..d7470e69 100644 --- a/api/modules/wireguard.py +++ b/api/modules/wireguard.py @@ -1,5 +1,6 @@ import subprocess import re +import os def configs(): #ignore symlinks, because wg0.conf is in production the main config, but in insiders it is a symlink @@ -24,13 +25,16 @@ def client_config_list(client_config): if not re.match(pattern, client_config): raise ValueError("Invalid client_config") - config_path = f"/etc/wireguard/{client_config}" - try: - with open(config_path, 'r') as f: - output = f.read().strip() - return output.split('\n') - except FileNotFoundError: + # sanitize path to prevent directory traversal + client_config = os.path.basename(client_config) + + config_path = os.path.join("/etc/wireguard/", client_config) + if not os.path.exists(config_path): raise FileNotFoundError("Client configuration file not found") + with open(config_path, 'r') as f: + output = f.read().strip() + return output.split('\n') + #TODO: where is the logfile?? #TODO: is service connected? From 79d33db2bf3490e32a613499b4ad0a95bc7a8016 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 11:27:44 +0100 Subject: [PATCH 40/51] Revert "Sanitize path to prevent directory traversal" This reverts commit 2cdf6ef53e8c5ee2f48da652143fdfe5776d1663. --- .gitignore | 1 + api/modules/wireguard.py | 16 ++++++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index a2a77b49..245d6fa4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ yarn-error.log includes/config.php rootCA.pem vendor +.env diff --git a/api/modules/wireguard.py b/api/modules/wireguard.py index d7470e69..904d87bb 100644 --- a/api/modules/wireguard.py +++ b/api/modules/wireguard.py @@ -1,6 +1,5 @@ import subprocess import re -import os def configs(): #ignore symlinks, because wg0.conf is in production the main config, but in insiders it is a symlink @@ -25,16 +24,13 @@ def client_config_list(client_config): if not re.match(pattern, client_config): raise ValueError("Invalid client_config") - # sanitize path to prevent directory traversal - client_config = os.path.basename(client_config) - - config_path = os.path.join("/etc/wireguard/", client_config) - if not os.path.exists(config_path): + config_path = f"/etc/wireguard/{client_config}" + try: + with open(config_path, 'r') as f: + output = f.read().strip() + return output.split('\n') + except FileNotFoundError: raise FileNotFoundError("Client configuration file not found") - with open(config_path, 'r') as f: - output = f.read().strip() - return output.split('\n') - #TODO: where is the logfile?? #TODO: is service connected? From 49780d8ec9072c078f42ef13a729b83ae082fa3f Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 20:12:30 +0100 Subject: [PATCH 41/51] Rename tag hostpost -> hotspot --- api/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/main.py b/api/main.py index edd02a42..d202167f 100644 --- a/api/main.py +++ b/api/main.py @@ -45,7 +45,7 @@ async def get_system(api_key: APIKey = Depends(auth.get_api_key)): 'rpiRevision': system.rpiRevision() } -@app.get("/ap", tags=["accesspoint/hostpost"]) +@app.get("/ap", tags=["accesspoint/hotspot"]) async def get_ap(api_key: APIKey = Depends(auth.get_api_key)): return{ 'driver': ap.driver(), From 19fe43466ef843e17dfe83da0f172ed68a28226c Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 20:13:20 +0100 Subject: [PATCH 42/51] Remove --reload from ExecStart --- installers/restapi.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installers/restapi.service b/installers/restapi.service index 4fccd105..92026322 100644 --- a/installers/restapi.service +++ b/installers/restapi.service @@ -6,7 +6,7 @@ After=network.target User=pi WorkingDirectory=/etc/raspap/api LimitNOFILE=4096 -ExecStart=/usr/bin/python3 -m uvicorn main:app --host 0.0.0.0 --port 8081 --reload +ExecStart=/usr/bin/python3 -m uvicorn main:app --host 0.0.0.0 --port 8081 ExecStop=/bin/kill -HUP ${MAINPID} Restart=on-failure RestartSec=5s From f61e1b5c1af33c63d92b0da5f57e72dc32cfc1c2 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 20:14:08 +0100 Subject: [PATCH 43/51] Clean up serviceLog output --- includes/restapi.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/restapi.php b/includes/restapi.php index ff16c997..2a1be6d8 100644 --- a/includes/restapi.php +++ b/includes/restapi.php @@ -46,6 +46,7 @@ function DisplayRestAPI() $serviceStatus = !empty($output) ? "up" : "down"; exec("sudo systemctl status restapi.service", $output, $return); + array_shift($output); $serviceLog = implode("\n", $output); echo renderTemplate("restapi", compact( From 9c5c0cfb88802df19f34110c25c7765a970be7e3 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 8 Mar 2024 20:17:22 +0100 Subject: [PATCH 44/51] Remove /wireguard/{config} endpoint --- api/main.py | 5 ----- api/modules/wireguard.py | 13 ------------- 2 files changed, 18 deletions(-) diff --git a/api/main.py b/api/main.py index d202167f..8e98647d 100644 --- a/api/main.py +++ b/api/main.py @@ -154,8 +154,3 @@ async def get_wireguard(api_key: APIKey = Depends(auth.get_api_key)): 'client_config_active': wireguard.client_config_active() } -@app.get("/wireguard/{config}", tags=["WireGuard"]) -async def client_config_list(config, api_key: APIKey = Depends(auth.get_api_key)): - return{ -'client_config': wireguard.client_config_list(config) -} diff --git a/api/modules/wireguard.py b/api/modules/wireguard.py index 904d87bb..36eaa2b6 100644 --- a/api/modules/wireguard.py +++ b/api/modules/wireguard.py @@ -19,18 +19,5 @@ def client_config_active(): active_config = output.split("/etc/wireguard/") return(active_config[1]) -def client_config_list(client_config): - pattern = r'^[a-zA-Z0-9_-]+$' - if not re.match(pattern, client_config): - raise ValueError("Invalid client_config") - - config_path = f"/etc/wireguard/{client_config}" - try: - with open(config_path, 'r') as f: - output = f.read().strip() - return output.split('\n') - except FileNotFoundError: - raise FileNotFoundError("Client configuration file not found") - #TODO: where is the logfile?? #TODO: is service connected? From 1e840abbaff03de204bdb8cf58d5ad79a2b0e5dd Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 9 Mar 2024 09:17:05 +0100 Subject: [PATCH 45/51] Update installer switches --- installers/common.sh | 2 +- installers/raspbian.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/installers/common.sh b/installers/common.sh index bec35c32..9cb11e5e 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -505,7 +505,7 @@ function _prompt_install_openvpn() { # Prompt to install restapi function _prompt_install_restapi() { - _install_log "Configure restapi" + _install_log "Configure RestAPI" echo -n "Install and enable RestAPI? [Y/n]: " if [ "$assume_yes" == 0 ]; then read answer < /dev/tty diff --git a/installers/raspbian.sh b/installers/raspbian.sh index 29b14e25..0626faab 100755 --- a/installers/raspbian.sh +++ b/installers/raspbian.sh @@ -40,7 +40,7 @@ OPTIONS: -y, --yes, --assume-yes Assumes "yes" as an answer to all prompts -c, --cert, --certificate Installs an SSL certificate for lighttpd -o, --openvpn Used with -y, --yes, sets OpenVPN install option (0=no install) ---rest Used with -y, --yes, sets RestAPI install option (0=no install) +-s, --rest, --restapi Used with -y, --yes, sets RestAPI install option (0=no install) -a, --adblock Used with -y, --yes, sets Adblock install option (0=no install) -w, --wireguard Used with -y, --yes, sets WireGuard install option (0=no install) -e, --provider Used with -y, --yes, sets the VPN provider install option @@ -113,7 +113,7 @@ function _parse_params() { ovpn_option="$2" shift ;; - --api|--rest|--restapi) + -s|--rest|--restapi) restapi_option="$2" shift ;; From f81c68de26813da1df7c9e3614999891ee9487b1 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 9 Mar 2024 09:25:02 +0100 Subject: [PATCH 46/51] Add restAPI doc link when svc is active --- includes/restapi.php | 19 ++++++++++++++++++- templates/restapi/general.php | 7 ++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/includes/restapi.php b/includes/restapi.php index 2a1be6d8..1338d24d 100644 --- a/includes/restapi.php +++ b/includes/restapi.php @@ -49,11 +49,18 @@ function DisplayRestAPI() array_shift($output); $serviceLog = implode("\n", $output); + if ($serviceStatus == "up") { + $docUrl = getDocUrl(); + $faicon = ""; + $docMsg = sprintf(_("RestAPI docs are accessible here%s"),$docUrl, $faicon); + } + echo renderTemplate("restapi", compact( "status", "apiKey", "serviceStatus", - "serviceLog" + "serviceLog", + "docMsg" )); } @@ -72,3 +79,13 @@ function saveAPISettings($status, $apiKey, $dotenv) return $status; } +// Returns a url for fastapi's automatic docs +function getDocUrl() +{ + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://'; + $server_name = $_SERVER['SERVER_NAME']; + $port = 8081; + $url = $protocol . $server_name .':'. $port . '/docs'; + return $url; +} + diff --git a/templates/restapi/general.php b/templates/restapi/general.php index b39c293c..07f12406 100644 --- a/templates/restapi/general.php +++ b/templates/restapi/general.php @@ -1,8 +1,13 @@

-
+
+
+ +
+
+
From 3262e3030fd6c56e11f19257a51bb8db385ec38d Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 9 Mar 2024 09:25:34 +0100 Subject: [PATCH 47/51] Update en_US locale + compile .mo --- locale/en_US/LC_MESSAGES/messages.mo | Bin 42175 -> 42345 bytes locale/en_US/LC_MESSAGES/messages.po | 10 ++++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/locale/en_US/LC_MESSAGES/messages.mo b/locale/en_US/LC_MESSAGES/messages.mo index eb29660e28f67632d35ec1051347763aa286d6b6..31fa6b70e3fa29755cffd5bf1036c6eb9f0a31f9 100644 GIT binary patch delta 10928 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
3b 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 diff --git a/locale/en_US/LC_MESSAGES/messages.po b/locale/en_US/LC_MESSAGES/messages.po index 85e1195c..f24ab022 100644 --- a/locale/en_US/LC_MESSAGES/messages.po +++ b/locale/en_US/LC_MESSAGES/messages.po @@ -1542,12 +1542,18 @@ msgstr "Start RestAPI service" msgid "Stop RestAPI service" msgstr "Stop RestAPI service" +msgid "API Key" +msgstr "API Key" + msgid "Saving API key" msgstr "Saving API key" msgid "RestAPI status" msgstr "RestAPI status" -msgid "Current raspap-restapi.service status is displayed below." -msgstr "Current raspap-restapi.service status is displayed below." +msgid "Current restapi.service status is displayed below." +msgstr "Current restapi.service status is displayed below." + +msgid "RestAPI docs are accessible here%s" +msgstr "RestAPI docs are accessible here%s" From 220709b6984c63755b0a49b06c565db76c2c7cb7 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 9 Mar 2024 11:24:36 +0100 Subject: [PATCH 48/51] fix: mv api files /facepalm --- installers/common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installers/common.sh b/installers/common.sh index 9cb11e5e..0ec18fbf 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -584,7 +584,7 @@ function _create_openvpn_scripts() { # Install and enable RestAPI configuration option function _install_restapi() { _install_log "Installing and enabling RestAPI" - sudo mv -r "$webroot_dir/api" "$raspap_dir/api" || _install_status 1 "Unable to move api folder" + sudo mv "$webroot_dir/api" "$raspap_dir/api" || _install_status 1 "Unable to move api folder" if ! command -v python3 &> /dev/null; then echo "Python is not installed. Installing Python..." From c408a6f5f8d3dfffeaacf1199f8e33b9f4299a05 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 9 Mar 2024 12:21:31 +0100 Subject: [PATCH 49/51] Add message to en_US locale + compile --- locale/en_US/LC_MESSAGES/messages.mo | Bin 42345 -> 42431 bytes locale/en_US/LC_MESSAGES/messages.po | 3 +++ 2 files changed, 3 insertions(+) diff --git a/locale/en_US/LC_MESSAGES/messages.mo b/locale/en_US/LC_MESSAGES/messages.mo index 31fa6b70e3fa29755cffd5bf1036c6eb9f0a31f9..62b44fcdb9fb3b62d80dfb85dc07fb39b902490d 100644 GIT binary patch delta 10936 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 pjnEyqtrA>YTZ%}{+BHk76t!BV4Qi`Z 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 urestapi.service status is displayed below." msgid "RestAPI docs are accessible here%s" msgstr "RestAPI docs are accessible here%s" +msgid "Restarting restapi.service" +msgstr "Restarting restapi.service" + From 8d6b8174d1034005df04cee29c5da7305799aec8 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 9 Mar 2024 12:22:14 +0100 Subject: [PATCH 50/51] Bump position of sidebar item --- includes/sidebar.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/includes/sidebar.php b/includes/sidebar.php index ce2e6be2..b83b8764 100755 --- a/includes/sidebar.php +++ b/includes/sidebar.php @@ -80,18 +80,17 @@ - - - - - + + From d1be0caf54d8259152efb3d2b23ec8472fb7ecee Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 9 Mar 2024 12:23:20 +0100 Subject: [PATCH 51/51] Restart restapi.sevice on API key save --- includes/restapi.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/includes/restapi.php b/includes/restapi.php index 1338d24d..68207b2b 100644 --- a/includes/restapi.php +++ b/includes/restapi.php @@ -26,6 +26,10 @@ function DisplayRestAPI() $status->addMessage('Please enter a valid API key', 'danger'); } else { $return = saveAPISettings($status, $apiKey, $dotenv); + $status->addMessage('Restarting restapi.service', 'info'); + exec('sudo /bin/systemctl stop restapi.service', $return); + sleep(1); + exec('sudo /bin/systemctl start restapi.service', $return); } } } elseif (isset($_POST['StartRestAPIservice'])) { @@ -75,7 +79,6 @@ function saveAPISettings($status, $apiKey, $dotenv) { $status->addMessage('Saving API key', 'info'); $dotenv->set('RASPAP_API_KEY', $apiKey); - return $status; }