mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Merge branch 'dev' into enh-backup-repo
This commit is contained in:
commit
8312134fc6
41 changed files with 1733 additions and 1119 deletions
|
@ -36,7 +36,7 @@ full-tests:
|
|||
- *install_debs
|
||||
- yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns --force-diskspace
|
||||
script:
|
||||
- python3 -m pytest --cov=yunohost tests/ src/yunohost/tests/ --junitxml=report.xml
|
||||
- python3 -m pytest --cov=yunohost tests/ src/yunohost/tests/ data/hooks/diagnosis/ --junitxml=report.xml
|
||||
- cd tests
|
||||
- bash test_helpers.sh
|
||||
needs:
|
||||
|
@ -113,10 +113,10 @@ test-apps:
|
|||
test-appscatalog:
|
||||
extends: .test-stage
|
||||
script:
|
||||
- python3 -m pytest src/yunohost/tests/test_appscatalog.py
|
||||
- python3 -m pytest src/yunohost/tests/test_app_catalog.py
|
||||
only:
|
||||
changes:
|
||||
- src/yunohost/app.py
|
||||
- src/yunohost/app_calalog.py
|
||||
|
||||
test-appurl:
|
||||
extends: .test-stage
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
|
||||
<div align="center">
|
||||
|
||||

|
||||
[](https://gitlab.com/yunohost/yunohost/-/pipelines)
|
||||

|
||||
[](https://github.com/YunoHost/yunohost/blob/dev/LICENSE)
|
||||
[](https://mastodon.social/@yunohost)
|
||||
|
||||
|
|
241
bin/yunomdns
241
bin/yunomdns
|
@ -4,160 +4,152 @@
|
|||
Pythonic declaration of mDNS .local domains for YunoHost
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
import socket
|
||||
from time import sleep
|
||||
from typing import List, Dict
|
||||
|
||||
from zeroconf import Zeroconf, ServiceInfo
|
||||
import ifaddr
|
||||
from ipaddress import ip_address
|
||||
from zeroconf import Zeroconf, ServiceInfo, ServiceBrowser
|
||||
|
||||
# Helper command taken from Moulinette
|
||||
def check_output(args, stderr=subprocess.STDOUT, shell=True, **kwargs):
|
||||
"""Run command with arguments and return its output as a byte string
|
||||
Overwrite some of the arguments to capture standard error in the result
|
||||
and use shell by default before calling subprocess.check_output.
|
||||
|
||||
def get_network_local_interfaces() -> Dict[str, Dict[str, List[str]]]:
|
||||
"""
|
||||
return (
|
||||
subprocess.check_output(args, stderr=stderr, shell=shell, **kwargs)
|
||||
.decode("utf-8")
|
||||
.strip()
|
||||
)
|
||||
|
||||
# Helper command taken from Moulinette
|
||||
def _extract_inet(string, skip_netmask=False, skip_loopback=True):
|
||||
Returns interfaces with their associated local IPs
|
||||
"""
|
||||
Extract IP addresses (v4 and/or v6) from a string limited to one
|
||||
address by protocol
|
||||
|
||||
Keyword argument:
|
||||
string -- String to search in
|
||||
skip_netmask -- True to skip subnet mask extraction
|
||||
skip_loopback -- False to include addresses reserved for the
|
||||
loopback interface
|
||||
|
||||
Returns:
|
||||
A dict of {protocol: address} with protocol one of 'ipv4' or 'ipv6'
|
||||
|
||||
"""
|
||||
ip4_pattern = (
|
||||
r"((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}"
|
||||
)
|
||||
ip6_pattern = r"(((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::?((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)"
|
||||
ip4_pattern += r"/[0-9]{1,2})" if not skip_netmask else ")"
|
||||
ip6_pattern += r"/[0-9]{1,3})" if not skip_netmask else ")"
|
||||
result = {}
|
||||
|
||||
for m in re.finditer(ip4_pattern, string):
|
||||
addr = m.group(1)
|
||||
if skip_loopback and addr.startswith("127."):
|
||||
continue
|
||||
|
||||
# Limit to only one result
|
||||
result["ipv4"] = addr
|
||||
break
|
||||
|
||||
for m in re.finditer(ip6_pattern, string):
|
||||
addr = m.group(1)
|
||||
if skip_loopback and addr == "::1":
|
||||
continue
|
||||
|
||||
# Limit to only one result
|
||||
result["ipv6"] = addr
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
# Helper command taken from Moulinette
|
||||
def get_network_interfaces():
|
||||
|
||||
# Get network devices and their addresses (raw infos from 'ip addr')
|
||||
devices_raw = {}
|
||||
output = check_output("ip --brief a").split("\n")
|
||||
for line in output:
|
||||
line = line.split()
|
||||
iname = line[0]
|
||||
ips = ' '.join(line[2:])
|
||||
|
||||
devices_raw[iname] = ips
|
||||
|
||||
# Parse relevant informations for each of them
|
||||
devices = {
|
||||
name: _extract_inet(addrs)
|
||||
for name, addrs in devices_raw.items()
|
||||
if name != "lo"
|
||||
interfaces = {
|
||||
adapter.name: {
|
||||
"ipv4": [ip.ip for ip in adapter.ips if ip.is_IPv4 and ip_address(ip.ip).is_private],
|
||||
"ipv6": [ip.ip[0] for ip in adapter.ips if ip.is_IPv6 and ip_address(ip.ip[0]).is_private and not ip_address(ip.ip[0]).is_link_local],
|
||||
}
|
||||
for adapter in ifaddr.get_adapters()
|
||||
if adapter.name != "lo"
|
||||
}
|
||||
return interfaces
|
||||
|
||||
return devices
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Listener class, to detect duplicates on the network
|
||||
# Stores the list of servers in its list property
|
||||
class Listener:
|
||||
|
||||
def __init__(self):
|
||||
self.list = []
|
||||
|
||||
def remove_service(self, zeroconf, type, name):
|
||||
info = zeroconf.get_service_info(type, name)
|
||||
self.list.remove(info.server)
|
||||
|
||||
def update_service(self, zeroconf, type, name):
|
||||
pass
|
||||
|
||||
def add_service(self, zeroconf, type, name):
|
||||
info = zeroconf.get_service_info(type, name)
|
||||
self.list.append(info.server[:-1])
|
||||
|
||||
|
||||
def main() -> bool:
|
||||
###
|
||||
# CONFIG
|
||||
###
|
||||
|
||||
with open('/etc/yunohost/mdns.yml', 'r') as f:
|
||||
with open("/etc/yunohost/mdns.yml", "r") as f:
|
||||
config = yaml.safe_load(f) or {}
|
||||
updated = False
|
||||
|
||||
required_fields = ["interfaces", "domains"]
|
||||
required_fields = ["domains"]
|
||||
missing_fields = [field for field in required_fields if field not in config]
|
||||
interfaces = get_network_local_interfaces()
|
||||
|
||||
if missing_fields:
|
||||
print("The fields %s are required" % ', '.join(missing_fields))
|
||||
print(f"The fields {missing_fields} are required in mdns.yml")
|
||||
return False
|
||||
|
||||
if config['interfaces'] is None:
|
||||
print('No interface listed for broadcast.')
|
||||
sys.exit(0)
|
||||
if "interfaces" not in config:
|
||||
config["interfaces"] = [interface
|
||||
for interface, local_ips in interfaces.items()
|
||||
if local_ips["ipv4"]]
|
||||
|
||||
if 'yunohost.local' not in config['domains']:
|
||||
config['domains'].append('yunohost.local')
|
||||
if "ban_interfaces" in config:
|
||||
config["interfaces"] = [interface
|
||||
for interface in config["interfaces"]
|
||||
if interface not in config["ban_interfaces"]]
|
||||
|
||||
zcs = {}
|
||||
interfaces = get_network_interfaces()
|
||||
for interface in config['interfaces']:
|
||||
infos = [] # List of ServiceInfo objects, to feed Zeroconf
|
||||
ips = [] # Human-readable IPs
|
||||
b_ips = [] # Binary-convered IPs
|
||||
# Let's discover currently published .local domains accross the network
|
||||
zc = Zeroconf()
|
||||
listener = Listener()
|
||||
browser = ServiceBrowser(zc, "_device-info._tcp.local.", listener)
|
||||
sleep(2)
|
||||
browser.cancel()
|
||||
zc.close()
|
||||
|
||||
ipv4 = interfaces[interface]['ipv4'].split('/')[0]
|
||||
if ipv4:
|
||||
ips.append(ipv4)
|
||||
b_ips.append(socket.inet_pton(socket.AF_INET, ipv4))
|
||||
# Always attempt to publish yunohost.local
|
||||
if "yunohost.local" not in config["domains"]:
|
||||
config["domains"].append("yunohost.local")
|
||||
|
||||
ipv6 = interfaces[interface]['ipv6'].split('/')[0]
|
||||
if ipv6:
|
||||
ips.append(ipv6)
|
||||
b_ips.append(socket.inet_pton(socket.AF_INET6, ipv6))
|
||||
def find_domain_not_already_published(domain):
|
||||
|
||||
# Try domain.local ... but if it's already published by another entity,
|
||||
# try domain-2.local, domain-3.local, ...
|
||||
|
||||
i = 1
|
||||
domain_i = domain
|
||||
|
||||
while domain_i in listener.list:
|
||||
print(f"Uh oh, {domain_i} already exists on the network...")
|
||||
|
||||
i += 1
|
||||
domain_i = domain.replace(".local", f"-{i}.local")
|
||||
|
||||
return domain_i
|
||||
|
||||
config['domains'] = [find_domain_not_already_published(domain) for domain in config['domains']]
|
||||
|
||||
zcs: Dict[Zeroconf, List[ServiceInfo]] = {}
|
||||
|
||||
for interface in config["interfaces"]:
|
||||
|
||||
if interface not in interfaces:
|
||||
print(f"Interface {interface} listed in config file is not present on system.")
|
||||
continue
|
||||
|
||||
# Only broadcast IPv4 because IPv6 is buggy ... because we ain't using python3-ifaddr >= 0.1.7
|
||||
# Buster only ships 0.1.6
|
||||
# Bullseye ships 0.1.7
|
||||
# To be re-enabled once we're on bullseye...
|
||||
# ips: List[str] = interfaces[interface]["ipv4"] + interfaces[interface]["ipv6"]
|
||||
ips: List[str] = interfaces[interface]["ipv4"]
|
||||
|
||||
# If at least one IP is listed
|
||||
if ips:
|
||||
# Create a Zeroconf object, and store the ServiceInfos
|
||||
zc = Zeroconf(interfaces=ips)
|
||||
zcs[zc]=[]
|
||||
for d in config['domains']:
|
||||
d_domain=d.replace('.local','')
|
||||
if '.' in d_domain:
|
||||
print(d_domain+'.local: subdomains are not supported.')
|
||||
else:
|
||||
# Create a ServiceInfo object for each .local domain
|
||||
zcs[zc].append(ServiceInfo(
|
||||
type_='_device-info._tcp.local.',
|
||||
name=interface+': '+d_domain+'._device-info._tcp.local.',
|
||||
addresses=b_ips,
|
||||
port=80,
|
||||
server=d+'.',
|
||||
))
|
||||
print('Adding '+d+' with addresses '+str(ips)+' on interface '+interface)
|
||||
if not ips:
|
||||
continue
|
||||
|
||||
# Create a Zeroconf object, and store the ServiceInfos
|
||||
zc = Zeroconf(interfaces=ips) # type: ignore
|
||||
zcs[zc] = []
|
||||
|
||||
for d in config["domains"]:
|
||||
d_domain = d.replace(".local", "")
|
||||
if "." in d_domain:
|
||||
print(f"{d_domain}.local: subdomains are not supported.")
|
||||
continue
|
||||
# Create a ServiceInfo object for each .local domain
|
||||
zcs[zc].append(
|
||||
ServiceInfo(
|
||||
type_="_device-info._tcp.local.",
|
||||
name=f"{interface}: {d_domain}._device-info._tcp.local.",
|
||||
parsed_addresses=ips,
|
||||
port=80,
|
||||
server=f"{d}.",
|
||||
)
|
||||
)
|
||||
print(f"Adding {d} with addresses {ips} on interface {interface}")
|
||||
|
||||
# Run registration
|
||||
print("Registering...")
|
||||
for zc, infos in zcs.items():
|
||||
for info in infos:
|
||||
zc.register_service(info)
|
||||
zc.register_service(info, allow_name_change=True, cooperating_responders=True)
|
||||
|
||||
try:
|
||||
print("Registered. Press Ctrl+C or stop service to stop.")
|
||||
|
@ -168,6 +160,11 @@ if __name__ == '__main__':
|
|||
finally:
|
||||
print("Unregistering...")
|
||||
for zc, infos in zcs.items():
|
||||
for info in infos:
|
||||
zc.unregister_service(info)
|
||||
zc.unregister_all_services()
|
||||
zc.close()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(0 if main() else 1)
|
||||
|
|
|
@ -51,15 +51,15 @@ _ynh_app_config_get_one() {
|
|||
then
|
||||
bind=":/etc/yunohost/apps/$app/settings.yml"
|
||||
fi
|
||||
local bind_key="$(echo "$bind" | cut -d: -f1)"
|
||||
bind_key=${bind_key:-$short_setting}
|
||||
if [[ "$bind_key" == *">"* ]];
|
||||
local bind_key_="$(echo "$bind" | cut -d: -f1)"
|
||||
bind_key_=${bind_key_:-$short_setting}
|
||||
if [[ "$bind_key_" == *">"* ]];
|
||||
then
|
||||
bind_after="$(echo "${bind_key}" | cut -d'>' -f1)"
|
||||
bind_key="$(echo "${bind_key}" | cut -d'>' -f2)"
|
||||
bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)"
|
||||
bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)"
|
||||
fi
|
||||
local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)"
|
||||
old[$short_setting]="$(ynh_read_var_in_file --file="${bind_file}" --key="${bind_key}" --after="${bind_after}")"
|
||||
old[$short_setting]="$(ynh_read_var_in_file --file="${bind_file}" --key="${bind_key_}" --after="${bind_after}")"
|
||||
|
||||
fi
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ _ynh_app_config_apply_one() {
|
|||
cp "${!short_setting}" "$bind_file"
|
||||
fi
|
||||
ynh_store_file_checksum --file="$bind_file" --update_only
|
||||
ynh_print_info --message="File '$bind_file' overwrited with ${!short_setting}"
|
||||
ynh_print_info --message="File '$bind_file' overwritten with ${!short_setting}"
|
||||
fi
|
||||
|
||||
# Save value in app settings
|
||||
|
@ -124,27 +124,27 @@ _ynh_app_config_apply_one() {
|
|||
ynh_backup_if_checksum_is_different --file="$bind_file"
|
||||
echo "${!short_setting}" > "$bind_file"
|
||||
ynh_store_file_checksum --file="$bind_file" --update_only
|
||||
ynh_print_info --message="File '$bind_file' overwrited with the content you provieded in '${short_setting}' question"
|
||||
ynh_print_info --message="File '$bind_file' overwritten with the content provided in question '${short_setting}'"
|
||||
|
||||
# Set value into a kind of key/value file
|
||||
else
|
||||
local bind_after=""
|
||||
local bind_key="$(echo "$bind" | cut -d: -f1)"
|
||||
bind_key=${bind_key:-$short_setting}
|
||||
if [[ "$bind_key" == *">"* ]];
|
||||
local bind_key_="$(echo "$bind" | cut -d: -f1)"
|
||||
bind_key_=${bind_key_:-$short_setting}
|
||||
if [[ "$bind_key_" == *">"* ]];
|
||||
then
|
||||
bind_after="$(echo "${bind_key}" | cut -d'>' -f1)"
|
||||
bind_key="$(echo "${bind_key}" | cut -d'>' -f2)"
|
||||
bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)"
|
||||
bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)"
|
||||
fi
|
||||
local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)"
|
||||
|
||||
ynh_backup_if_checksum_is_different --file="$bind_file"
|
||||
ynh_write_var_in_file --file="${bind_file}" --key="${bind_key}" --value="${!short_setting}" --after="${bind_after}"
|
||||
ynh_write_var_in_file --file="${bind_file}" --key="${bind_key_}" --value="${!short_setting}" --after="${bind_after}"
|
||||
ynh_store_file_checksum --file="$bind_file" --update_only
|
||||
|
||||
# We stored the info in settings in order to be able to upgrade the app
|
||||
ynh_app_setting_set --app=$app --key=$short_setting --value="${!short_setting}"
|
||||
ynh_print_info --message="Configuration key '$bind_key' edited into $bind_file"
|
||||
ynh_print_info --message="Configuration key '$bind_key_' edited into $bind_file"
|
||||
|
||||
fi
|
||||
fi
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
n_version=7.3.0
|
||||
n_version=7.5.0
|
||||
n_install_dir="/opt/node_n"
|
||||
node_version_path="$n_install_dir/n/versions/node"
|
||||
# N_PREFIX is the directory of n, it needs to be loaded as a environment variable.
|
||||
|
@ -17,7 +17,7 @@ ynh_install_n () {
|
|||
ynh_print_info --message="Installation of N - Node.js version management"
|
||||
# Build an app.src for n
|
||||
echo "SOURCE_URL=https://github.com/tj/n/archive/v${n_version}.tar.gz
|
||||
SOURCE_SUM=b908b0fc86922ede37e89d1030191285209d7d521507bf136e62895e5797847f" > "$YNH_APP_BASEDIR/conf/n.src"
|
||||
SOURCE_SUM=d4da7ea91f680de0c9b5876e097e2a793e8234fcd0f7ca87a0599b925be087a3" > "$YNH_APP_BASEDIR/conf/n.src"
|
||||
# Download and extract n
|
||||
ynh_setup_source --dest_dir="$n_install_dir/git" --source_id=n
|
||||
# Install n
|
||||
|
|
|
@ -12,13 +12,6 @@ _generate_config() {
|
|||
[[ "$domain" =~ ^[^.]+\.local$ ]] || continue
|
||||
echo " - $domain"
|
||||
done
|
||||
|
||||
echo "interfaces:"
|
||||
local_network_interfaces="$(ip --brief a | grep ' 10\.\| 192\.168\.' | awk '{print $1}')"
|
||||
for interface in $local_network_interfaces
|
||||
do
|
||||
echo " - $interface"
|
||||
done
|
||||
}
|
||||
|
||||
do_init_regen() {
|
||||
|
|
|
@ -8,13 +8,16 @@ from publicsuffix import PublicSuffixList
|
|||
|
||||
from moulinette.utils.process import check_output
|
||||
|
||||
from yunohost.utils.dns import dig, YNH_DYNDNS_DOMAINS
|
||||
from yunohost.utils.dns import (
|
||||
dig,
|
||||
YNH_DYNDNS_DOMAINS,
|
||||
is_yunohost_dyndns_domain,
|
||||
is_special_use_tld,
|
||||
)
|
||||
from yunohost.diagnosis import Diagnoser
|
||||
from yunohost.domain import domain_list, _get_maindomain
|
||||
from yunohost.dns import _build_dns_conf, _get_dns_zone_for_domain
|
||||
|
||||
SPECIAL_USE_TLDS = ["local", "localhost", "onion", "test"]
|
||||
|
||||
|
||||
class DNSRecordsDiagnoser(Diagnoser):
|
||||
|
||||
|
@ -26,23 +29,20 @@ class DNSRecordsDiagnoser(Diagnoser):
|
|||
|
||||
main_domain = _get_maindomain()
|
||||
|
||||
all_domains = domain_list(exclude_subdomains=True)["domains"]
|
||||
for domain in all_domains:
|
||||
major_domains = domain_list(exclude_subdomains=True)["domains"]
|
||||
for domain in major_domains:
|
||||
self.logger_debug("Diagnosing DNS conf for %s" % domain)
|
||||
is_specialusedomain = any(
|
||||
domain.endswith("." + tld) for tld in SPECIAL_USE_TLDS
|
||||
)
|
||||
|
||||
for report in self.check_domain(
|
||||
domain,
|
||||
domain == main_domain,
|
||||
is_specialusedomain=is_specialusedomain,
|
||||
):
|
||||
yield report
|
||||
|
||||
# Check if a domain buy by the user will expire soon
|
||||
psl = PublicSuffixList()
|
||||
domains_from_registrar = [
|
||||
psl.get_public_suffix(domain) for domain in all_domains
|
||||
psl.get_public_suffix(domain) for domain in major_domains
|
||||
]
|
||||
domains_from_registrar = [
|
||||
domain for domain in domains_from_registrar if "." in domain
|
||||
|
@ -53,7 +53,16 @@ class DNSRecordsDiagnoser(Diagnoser):
|
|||
for report in self.check_expiration_date(domains_from_registrar):
|
||||
yield report
|
||||
|
||||
def check_domain(self, domain, is_main_domain, is_specialusedomain):
|
||||
def check_domain(self, domain, is_main_domain):
|
||||
|
||||
if is_special_use_tld(domain):
|
||||
categories = []
|
||||
yield dict(
|
||||
meta={"domain": domain},
|
||||
data={},
|
||||
status="INFO",
|
||||
summary="diagnosis_dns_specialusedomain",
|
||||
)
|
||||
|
||||
base_dns_zone = _get_dns_zone_for_domain(domain)
|
||||
basename = domain.replace(base_dns_zone, "").rstrip(".") or "@"
|
||||
|
@ -64,15 +73,6 @@ class DNSRecordsDiagnoser(Diagnoser):
|
|||
|
||||
categories = ["basic", "mail", "xmpp", "extra"]
|
||||
|
||||
if is_specialusedomain:
|
||||
categories = []
|
||||
yield dict(
|
||||
meta={"domain": domain},
|
||||
data={},
|
||||
status="INFO",
|
||||
summary="diagnosis_dns_specialusedomain",
|
||||
)
|
||||
|
||||
for category in categories:
|
||||
|
||||
records = expected_configuration[category]
|
||||
|
@ -84,7 +84,8 @@ class DNSRecordsDiagnoser(Diagnoser):
|
|||
id_ = r["type"] + ":" + r["name"]
|
||||
fqdn = r["name"] + "." + base_dns_zone if r["name"] != "@" else domain
|
||||
|
||||
# Ugly hack to not check mail records for subdomains stuff, otherwise will end up in a shitstorm of errors for people with many subdomains...
|
||||
# Ugly hack to not check mail records for subdomains stuff,
|
||||
# otherwise will end up in a shitstorm of errors for people with many subdomains...
|
||||
# Should find a cleaner solution in the suggested conf...
|
||||
if r["type"] in ["MX", "TXT"] and fqdn not in [
|
||||
domain,
|
||||
|
@ -131,6 +132,12 @@ class DNSRecordsDiagnoser(Diagnoser):
|
|||
status = "SUCCESS"
|
||||
summary = "diagnosis_dns_good_conf"
|
||||
|
||||
# If status is okay and there's actually no expected records
|
||||
# (e.g. XMPP disabled)
|
||||
# then let's not yield any diagnosis line
|
||||
if not records and "status" == "SUCCESS":
|
||||
continue
|
||||
|
||||
output = dict(
|
||||
meta={"domain": domain, "category": category},
|
||||
data=results,
|
||||
|
@ -140,10 +147,7 @@ class DNSRecordsDiagnoser(Diagnoser):
|
|||
|
||||
if discrepancies:
|
||||
# For ynh-managed domains (nohost.me etc...), tell people to try to "yunohost dyndns update --force"
|
||||
if any(
|
||||
domain.endswith(ynh_dyndns_domain)
|
||||
for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS
|
||||
):
|
||||
if is_yunohost_dyndns_domain(domain):
|
||||
output["details"] = ["diagnosis_dns_try_dyndns_update_force"]
|
||||
# Otherwise point to the documentation
|
||||
else:
|
||||
|
|
|
@ -8,6 +8,7 @@ from moulinette.utils.filesystem import read_file
|
|||
|
||||
from yunohost.diagnosis import Diagnoser
|
||||
from yunohost.domain import domain_list
|
||||
from yunohost.utils.dns import is_special_use_tld
|
||||
|
||||
DIAGNOSIS_SERVER = "diagnosis.yunohost.org"
|
||||
|
||||
|
@ -34,11 +35,11 @@ class WebDiagnoser(Diagnoser):
|
|||
summary="diagnosis_http_nginx_conf_not_up_to_date",
|
||||
details=["diagnosis_http_nginx_conf_not_up_to_date_details"],
|
||||
)
|
||||
elif domain.endswith(".local"):
|
||||
elif is_special_use_tld(domain):
|
||||
yield dict(
|
||||
meta={"domain": domain},
|
||||
status="INFO",
|
||||
summary="diagnosis_http_localdomain",
|
||||
summary="diagnosis_http_special_use_tld",
|
||||
)
|
||||
else:
|
||||
domains_to_check.append(domain)
|
||||
|
|
|
@ -76,7 +76,7 @@ class AppDiagnoser(Diagnoser):
|
|||
for deprecated_helper in deprecated_helpers:
|
||||
if (
|
||||
os.system(
|
||||
f"grep -hr '{deprecated_helper}' {app['setting_path']}/scripts/ | grep -v -q '^\s*#'"
|
||||
f"grep -hr '{deprecated_helper}' {app['setting_path']}/scripts/ | grep -v -q '^\\s*#'"
|
||||
)
|
||||
== 0
|
||||
):
|
||||
|
|
|
@ -6,6 +6,7 @@ After=network.target
|
|||
User=mdns
|
||||
Group=mdns
|
||||
Type=simple
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
ExecStart=/usr/bin/yunomdns
|
||||
StandardOutput=syslog
|
||||
|
||||
|
|
25
debian/changelog
vendored
25
debian/changelog
vendored
|
@ -1,3 +1,28 @@
|
|||
yunohost (4.3.1.1) testing; urgency=low
|
||||
|
||||
- [enh] app helpers: Update n version ([#1347](https://github.com/YunoHost/yunohost/pull/1347))
|
||||
- [enh] Misc app.py refactoring + Prevent change_url from being used to move a fulldomain app to a subpath ([#1346](https://github.com/YunoHost/yunohost/pull/1346))
|
||||
- [i18n] Translations updated for French, Galician, Portuguese, Ukrainian
|
||||
|
||||
Thanks to all contributors <3 ! (Éric Gaspar, José M, mifegui, ppr, Tymofii-Lytvynenko)
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 04 Oct 2021 01:33:22 +0200
|
||||
|
||||
yunohost (4.3.1) testing; urgency=low
|
||||
|
||||
- [fix] diagnosis: new app diagnosis grep reporing comments as issues ([#1333](https://github.com/YunoHost/yunohost/pull/1333))
|
||||
- [enh] configpanel: Bind function for hotspot (79126809)
|
||||
- [enh] cli: Rework/improve prompt mecanic ([#1338](https://github.com/YunoHost/yunohost/pull/1338))
|
||||
- [fix] dyndns update broke because of buggy dns record names (da1b9089)
|
||||
- [enh] dns: general improvement for special-use TLD / ynh dyndns domains (17aafe6f)
|
||||
- [fix] yunomdns: various fixes/improvements ([#1335](https://github.com/YunoHost/yunohost/pull/1335))
|
||||
- [fix] certs: Adapt ready_for_ACME check to the new dnsrecord result format... (d75c1a61)
|
||||
- [i18n] Translations updated for French
|
||||
|
||||
Thanks to all contributors <3 ! (Éric Gaspar, Félix Piédallu, Kayou, ljf, tituspijean)
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 29 Sep 2021 22:22:42 +0200
|
||||
|
||||
yunohost (4.3.0) testing; urgency=low
|
||||
|
||||
- [users] Import/export users from/to CSV ([#1089](https://github.com/YunoHost/yunohost/pull/1089))
|
||||
|
|
2
debian/control
vendored
2
debian/control
vendored
|
@ -10,7 +10,7 @@ Package: yunohost
|
|||
Essential: yes
|
||||
Architecture: all
|
||||
Depends: ${python3:Depends}, ${misc:Depends}
|
||||
, moulinette (>= 4.2), ssowat (>= 4.0)
|
||||
, moulinette (>= 4.3), ssowat (>= 4.3)
|
||||
, python3-psutil, python3-requests, python3-dnspython, python3-openssl
|
||||
, python3-miniupnpc, python3-dbus, python3-jinja2
|
||||
, python3-toml, python3-packaging, python3-publicsuffix,
|
||||
|
|
|
@ -36,7 +36,6 @@
|
|||
"app_manifest_install_ask_is_public": "Should this app be exposed to anonymous visitors?",
|
||||
"app_manifest_install_ask_password": "Choose an administration password for this app",
|
||||
"app_manifest_install_ask_path": "Choose the URL path (after the domain) where this app should be installed",
|
||||
"app_manifest_invalid": "Something is wrong with the app manifest: {error}",
|
||||
"app_not_correctly_installed": "{app} seems to be incorrectly installed",
|
||||
"app_not_installed": "Could not find {app} in the list of installed apps: {all_apps}",
|
||||
"app_not_properly_removed": "{app} has not been properly removed",
|
||||
|
@ -192,7 +191,7 @@
|
|||
"diagnosis_dns_good_conf": "DNS records are correctly configured for domain {domain} (category {category})",
|
||||
"diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.<br>Type: <code>{type}</code><br>Name: <code>{name}</code><br>Value: <code>{value}</code>",
|
||||
"diagnosis_dns_point_to_doc": "Please check the documentation at <a href='https://yunohost.org/dns_config'>https://yunohost.org/dns_config</a> if you need help about configuring DNS records.",
|
||||
"diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) and is therefore not expected to have actual DNS records.",
|
||||
"diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.",
|
||||
"diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using <cmd>yunohost dyndns update --force</cmd>.",
|
||||
"diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!",
|
||||
"diagnosis_domain_expiration_not_found": "Unable to check the expiration date for some domains",
|
||||
|
@ -201,7 +200,7 @@
|
|||
"diagnosis_domain_expiration_warning": "Some domains will expire soon!",
|
||||
"diagnosis_domain_expires_in": "{domain} expires in {days} days.",
|
||||
"diagnosis_domain_not_found_details": "The domain {domain} doesn't exist in WHOIS database or is expired!",
|
||||
"diagnosis_everything_ok": "Everything looks good for {category}!",
|
||||
"diagnosis_everything_ok": "Everything looks OK for {category}!",
|
||||
"diagnosis_failed": "Failed to fetch diagnosis result for category '{category}': {error}",
|
||||
"diagnosis_failed_for_category": "Diagnosis failed for category '{category}': {error}",
|
||||
"diagnosis_found_errors": "Found {errors} significant issue(s) related to {category}!",
|
||||
|
@ -214,7 +213,7 @@
|
|||
"diagnosis_http_could_not_diagnose_details": "Error: {error}",
|
||||
"diagnosis_http_hairpinning_issue": "Your local network does not seem to have hairpinning enabled.",
|
||||
"diagnosis_http_hairpinning_issue_details": "This is probably because of your ISP box / router. As a result, people from outside your local network will be able to access your server as expected, but not people from inside the local network (like you, probably?) when using the domain name or global IP. You may be able to improve the situation by having a look at <a href='https://yunohost.org/dns_local_network'>https://yunohost.org/dns_local_network</a>",
|
||||
"diagnosis_http_localdomain": "Domain {domain}, with a .local TLD, is not expected to be exposed outside the local network.",
|
||||
"diagnosis_http_special_use_tld": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to be exposed outside the local network.",
|
||||
"diagnosis_http_nginx_conf_not_up_to_date": "This domain's nginx configuration appears to have been modified manually, and prevents YunoHost from diagnosing if it's reachable on HTTP.",
|
||||
"diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference with the command line using <cmd>yunohost tools regen-conf nginx --dry-run --with-diff</cmd> and if you're ok, apply the changes with <cmd>yunohost tools regen-conf nginx --force</cmd>.",
|
||||
"diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.",
|
||||
|
@ -308,6 +307,7 @@
|
|||
"domain_deleted": "Domain deleted",
|
||||
"domain_deletion_failed": "Unable to delete domain {domain}: {error}",
|
||||
"domain_dns_conf_is_just_a_recommendation": "This command shows you the *recommended* configuration. It does not actually set up the DNS configuration for you. It is your responsability to configure your DNS zone in your registrar according to this recommendation.",
|
||||
"domain_dns_conf_special_use_tld": "This domain is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.",
|
||||
"domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain",
|
||||
"domain_dyndns_root_unknown": "Unknown DynDNS root domain",
|
||||
"domain_exists": "The domain already exists",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"admin_password_change_failed": "Impossible de changer le mot de passe",
|
||||
"admin_password_changed": "Le mot de passe d'administration a été modifié",
|
||||
"app_already_installed": "{app} est déjà installé",
|
||||
"app_argument_choice_invalid": "Choix invalide pour le paramètre '{name}'. Les valeurs acceptées sont {choices}, au lieu de '{value}'",
|
||||
"app_argument_choice_invalid": "Choisissez une valeur valide pour l'argument '{name}' : '{value}' ne fait pas partie des choix disponibles ({choices})",
|
||||
"app_argument_invalid": "Valeur invalide pour le paramètre '{name}' : {error}",
|
||||
"app_argument_required": "Le paramètre '{name}' est requis",
|
||||
"app_extraction_failed": "Impossible d'extraire les fichiers d'installation",
|
||||
|
@ -431,7 +431,7 @@
|
|||
"diagnosis_cache_still_valid": "(Le cache est encore valide pour le diagnostic {category}. Il ne sera pas re-diagnostiqué pour le moment !)",
|
||||
"diagnosis_ignored_issues": "(+ {nb_ignored} problème(s) ignoré(s))",
|
||||
"diagnosis_found_warnings": "Trouvé {warnings} objet(s) pouvant être amélioré(s) pour {category}.",
|
||||
"diagnosis_everything_ok": "Tout semble bien pour {category} !",
|
||||
"diagnosis_everything_ok": "Tout semble OK pour {category} !",
|
||||
"diagnosis_failed": "Échec de la récupération du résultat du diagnostic pour la catégorie '{category}' : {error}",
|
||||
"diagnosis_ip_connected_ipv4": "Le serveur est connecté à Internet en IPv4 !",
|
||||
"diagnosis_ip_no_ipv4": "Le serveur ne dispose pas d'une adresse IPv4.",
|
||||
|
@ -593,7 +593,7 @@
|
|||
"diagnosis_package_installed_from_sury": "Des paquets du système devraient être rétrogradé de version",
|
||||
"additional_urls_already_added": "URL supplémentaire '{url}' déjà ajoutée pour la permission '{permission}'",
|
||||
"unknown_main_domain_path": "Domaine ou chemin inconnu pour '{app}'. Vous devez spécifier un domaine et un chemin pour pouvoir spécifier une URL pour l'autorisation.",
|
||||
"show_tile_cant_be_enabled_for_regex": "Vous ne pouvez pas activer 'show_tile' pour le moment, car l'URL de l'autorisation '{permission}' est une expression régulière",
|
||||
"show_tile_cant_be_enabled_for_regex": "Vous ne pouvez pas activer 'show_tile' pour le moment, cela car l'URL de l'autorisation '{permission}' est une expression régulière",
|
||||
"show_tile_cant_be_enabled_for_url_not_defined": "Vous ne pouvez pas activer 'show_tile' pour le moment, car vous devez d'abord définir une URL pour l'autorisation '{permission}'",
|
||||
"regex_with_only_domain": "Vous ne pouvez pas utiliser une expression régulière pour le domaine, uniquement pour le chemin",
|
||||
"regex_incompatible_with_tile": "/!\\ Packagers ! La permission '{permission}' a 'show_tile' définie sur 'true' et vous ne pouvez donc pas définir une URL regex comme URL principale",
|
||||
|
@ -632,7 +632,7 @@
|
|||
"global_settings_setting_security_webadmin_allowlist": "Adresses IP autorisées à accéder à la webadmin. Elles doivent être séparées par une virgule.",
|
||||
"global_settings_setting_security_webadmin_allowlist_enabled": "Autoriser seulement certaines IP à accéder à la webadmin.",
|
||||
"diagnosis_http_localdomain": "Le domaine {domain}, avec un TLD .local, ne devrait pas être exposé en dehors du réseau local.",
|
||||
"diagnosis_dns_specialusedomain": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial et ne devrait donc pas avoir d'enregistrements DNS réels.",
|
||||
"diagnosis_dns_specialusedomain": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial comme .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.",
|
||||
"invalid_password": "Mot de passe incorrect",
|
||||
"ldap_server_is_down_restart_it": "Le service LDAP est en panne, essayez de le redémarrer...",
|
||||
"ldap_server_down": "Impossible d'atteindre le serveur LDAP",
|
||||
|
@ -675,5 +675,39 @@
|
|||
"log_app_config_set": "Appliquer la configuration à l'application '{}'",
|
||||
"service_not_reloading_because_conf_broken": "Le service '{name}' n'a pas été rechargé/redémarré car sa configuration est cassée : {errors}",
|
||||
"app_argument_password_help_keep": "Tapez sur Entrée pour conserver la valeur actuelle",
|
||||
"app_argument_password_help_optional": "Tapez un espace pour vider le mot de passe"
|
||||
}
|
||||
"app_argument_password_help_optional": "Tapez un espace pour vider le mot de passe",
|
||||
"domain_registrar_is_not_configured": "Le registrar n'est pas encore configuré pour le domaine {domain}.",
|
||||
"domain_dns_push_not_applicable": "La fonction de configuration DNS automatique n'est pas applicable au domaine {domain}. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns_config.",
|
||||
"domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost dyndns update')",
|
||||
"domain_dns_registrar_supported": "YunoHost a détecté automatiquement que ce domaine est géré par le registrar **{registrar}**. Si vous le souhaitez, YunoHost configurera automatiquement cette zone DNS, si vous lui fournissez les identifiants API appropriés. Vous pouvez trouver de la documentation sur la façon d'obtenir vos identifiants API sur cette page : https://yunohost.org/registar_api_{registrar}. (Vous pouvez également configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns )",
|
||||
"domain_config_features_disclaimer": "Jusqu'à présent, l'activation/désactivation des fonctionnalités de messagerie ou XMPP n'a d'impact que sur la configuration DNS recommandée et automatique, et non sur les configurations système !",
|
||||
"domain_dns_push_managed_in_parent_domain": "La fonctionnalité de configuration DNS automatique est gérée dans le domaine parent {parent_domain}.",
|
||||
"domain_dns_registrar_managed_in_parent_domain": "Ce domaine est un sous-domaine de {parent_domain_link}. La configuration du registrar DNS doit être gérée dans le panneau de configuration de {parent_domain}.",
|
||||
"domain_dns_registrar_not_supported": "YunoHost n'a pas pu détecter automatiquement le bureau d'enregistrement gérant ce domaine. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns.",
|
||||
"domain_dns_registrar_experimental": "Jusqu'à présent, l'interface avec l'API de **{registrar}** n'a pas été correctement testée et revue par la communauté YunoHost. L'assistance est **très expérimentale** - soyez prudent !",
|
||||
"domain_dns_push_failed_to_authenticate": "Échec de l'authentification sur l'API du bureau d'enregistrement pour le domaine « {domain} ». Très probablement les informations d'identification sont incorrectes ? (Erreur : {error})",
|
||||
"domain_dns_push_failed_to_list": "Échec de la liste des enregistrements actuels à l'aide de l'API du registraire : {error}",
|
||||
"domain_dns_push_already_up_to_date": "Dossiers déjà à jour.",
|
||||
"domain_dns_pushing": "Transmission des enregistrements DNS...",
|
||||
"domain_dns_push_record_failed": "Échec de l'enregistrement {action} {type}/{name} : {error}",
|
||||
"domain_dns_push_success": "Enregistrements DNS mis à jour !",
|
||||
"domain_dns_push_failed": "La mise à jour des enregistrements DNS a échoué.",
|
||||
"domain_dns_push_partial_failure": "Enregistrements DNS partiellement mis à jour : certains avertissements/erreurs ont été signalés.",
|
||||
"domain_config_mail_in": "Emails entrants",
|
||||
"domain_config_mail_out": "Emails sortants",
|
||||
"domain_config_xmpp": "Messagerie instantanée (XMPP)",
|
||||
"domain_config_auth_token": "Jeton d'authentification",
|
||||
"domain_config_auth_key": "Clé d'authentification",
|
||||
"domain_config_auth_secret": "Secret d'authentification",
|
||||
"domain_config_api_protocol": "Protocole API",
|
||||
"domain_config_auth_entrypoint": "Point d'entrée API",
|
||||
"domain_config_auth_application_key": "Clé d'application",
|
||||
"domain_config_auth_application_secret": "Clé secrète de l'application",
|
||||
"ldap_attribute_already_exists": "L'attribut LDAP '{attribute}' existe déjà avec la valeur '{value}'",
|
||||
"log_domain_config_set": "Mettre à jour la configuration du domaine '{}'",
|
||||
"log_domain_dns_push": "Pousser les enregistrements DNS pour le domaine '{}'",
|
||||
"diagnosis_http_special_use_tld": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et n'est donc pas censé être exposé en dehors du réseau local.",
|
||||
"domain_dns_conf_special_use_tld": "Ce domaine est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.",
|
||||
"other_available_options": "... et {n} autres options disponibles non affichées",
|
||||
"domain_config_auth_consumer_key": "Consumer key"
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"app_argument_required": "Requírese o argumento '{name}'",
|
||||
"app_argument_password_no_default": "Erro ao procesar o argumento do contrasinal '{name}': o argumento do contrasinal non pode ter un valor por defecto por razón de seguridade",
|
||||
"app_argument_invalid": "Elixe un valor válido para o argumento '{name}': {error}",
|
||||
"app_argument_choice_invalid": "Usa unha destas opcións '{choices}' para o argumento '{name}' no lugar de '{value}'",
|
||||
"app_argument_choice_invalid": "Elixe un valor válido para o argumento '{name}': '{value}' non está entre as opcións dispoñibles ({choices})",
|
||||
"backup_archive_writing_error": "Non se puideron engadir os ficheiros '{source}' (chamados no arquivo '{dest}' para ser copiados dentro do arquivo comprimido '{archive}'",
|
||||
"backup_archive_system_part_not_available": "A parte do sistema '{part}' non está dispoñible nesta copia",
|
||||
"backup_archive_corrupted": "Semella que o arquivo de copia '{archive}' está estragado : {error}",
|
||||
|
@ -102,7 +102,7 @@
|
|||
"backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar o arquivo",
|
||||
"backup_cleaning_failed": "Non se puido baleirar o cartafol temporal para a copia",
|
||||
"backup_cant_mount_uncompress_archive": "Non se puido montar o arquivo sen comprimir porque está protexido contra escritura",
|
||||
"backup_ask_for_copying_if_needed": "Queres realizar a copia de apoio utilizando temporalmente {size}MB? (Faise deste xeito porque algúns ficheiros non hai xeito de preparalos usando unha forma máis eficiente).",
|
||||
"backup_ask_for_copying_if_needed": "Queres realizar a copia de apoio utilizando temporalmente {size}MB? (Faise deste xeito porque algúns ficheiros non hai xeito de preparalos usando unha forma máis eficiente.)",
|
||||
"backup_running_hooks": "Executando os ganchos da copia...",
|
||||
"backup_permission": "Permiso de copia para {app}",
|
||||
"backup_output_symlink_dir_broken": "O directorio de arquivo '{path}' é unha ligazón simbólica rota. Pode ser que esqueceses re/montar ou conectar o medio de almacenaxe ao que apunta.",
|
||||
|
@ -455,7 +455,7 @@
|
|||
"migration_0015_modified_files": "Ten en conta que os seguintes ficheiros semella que foron modificados manualmente e poderían ser sobrescritos na actualización: {manually_modified_files}",
|
||||
"migration_0015_problematic_apps_warning": "Ten en conta que se detectaron as seguintes apps que poderían ser problemáticas. Semella que non foron instaladas usando o catálogo de YunoHost, ou non están marcadas como 'funcionais'. En consecuencia, non se pode garantir que seguirán funcionando após a actualización: {problematic_apps}",
|
||||
"diagnosis_http_localdomain": "O dominio {domain}, cun TLD .local, non é de agardar que esté exposto ao exterior da rede local.",
|
||||
"diagnosis_dns_specialusedomain": "O dominio {domain} baséase un dominio de nivel alto e uso especial (TLD) polo que non é de agardar que realmente teña rexistros DNS.",
|
||||
"diagnosis_dns_specialusedomain": "O dominio {domain} baséase un dominio de nivel alto e uso especial (TLD) como .local ou .test polo que non é de agardar que realmente teña rexistros DNS.",
|
||||
"upnp_enabled": "UPnP activado",
|
||||
"upnp_disabled": "UPnP desactivado",
|
||||
"permission_creation_failed": "Non se creou o permiso '{permission}': {error}",
|
||||
|
@ -675,5 +675,39 @@
|
|||
"config_version_not_supported": "A versión do panel de configuración '{version}' non está soportada.",
|
||||
"file_extension_not_accepted": "Rexeitouse o ficheiro '{path}' porque a súa extensión non está entre as aceptadas: {accept}",
|
||||
"invalid_number_max": "Ten que ser menor de {max}",
|
||||
"service_not_reloading_because_conf_broken": "Non se recargou/reiniciou o servizo '{name}' porque a súa configuración está estragada: {errors}"
|
||||
}
|
||||
"service_not_reloading_because_conf_broken": "Non se recargou/reiniciou o servizo '{name}' porque a súa configuración está estragada: {errors}",
|
||||
"diagnosis_http_special_use_tld": "O dominio {domain} baséase nun dominio de alto-nivel (TLD) especial como .local ou .test e por isto non é de agardar que esté exposto fóra da rede local.",
|
||||
"domain_dns_conf_special_use_tld": "Este dominio baséase nun dominio de alto-nivel (TLD) de uso especial como .local ou .test e por isto non é de agardar que teña rexistros DNS asociados.",
|
||||
"domain_dns_registrar_managed_in_parent_domain": "Este dominio é un subdominio de {parent_domain_link}. A configuración DNS debe xestionarse no panel de configuración de {parent_domain}'s.",
|
||||
"domain_dns_registrar_not_supported": "YunoHost non é quen de detectar a rexistradora que xestiona o dominio. Debes configurar manualmente os seus rexistros DNS seguindo a documentación en https://yunohost.org/dns.",
|
||||
"domain_dns_registrar_experimental": "Ata o momento, a interface coa API de **{registrar}** aínda non foi comprobada e revisada pola comunidade YunoHost. O soporte é **moi experimental** - ten coidado!",
|
||||
"domain_dns_push_failed_to_list": "Non se pode mostrar a lista actual de rexistros na API da rexistradora: {error}",
|
||||
"domain_dns_push_already_up_to_date": "Rexistros ao día, nada que facer.",
|
||||
"domain_dns_pushing": "Enviando rexistros DNS...",
|
||||
"domain_dns_push_record_failed": "Fallou {action} do rexistro {type}/{name}: {error}",
|
||||
"domain_dns_push_success": "Rexistros DNS actualizados!",
|
||||
"domain_dns_push_failed": "Fallou completamente a actualización dos rexistros DNS.",
|
||||
"domain_config_features_disclaimer": "Ata o momento, activar/desactivar as funcións de email ou XMPP só ten impacto na configuración automática da configuración DNS, non na configuración do sistema!",
|
||||
"domain_config_mail_in": "Emails entrantes",
|
||||
"domain_config_mail_out": "Emails saíntes",
|
||||
"domain_config_xmpp": "Mensaxería instantánea (XMPP)",
|
||||
"domain_config_auth_secret": "Segreda de autenticación",
|
||||
"domain_config_api_protocol": "Protocolo API",
|
||||
"domain_config_auth_application_key": "Chave da aplicación",
|
||||
"domain_config_auth_application_secret": "Chave segreda da aplicación",
|
||||
"domain_config_auth_consumer_key": "Chave consumidora",
|
||||
"log_domain_dns_push": "Enviar rexistros DNS para o dominio '{}'",
|
||||
"other_available_options": "... e outras {n} opcións dispoñibles non mostradas",
|
||||
"domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost dyndns update')",
|
||||
"domain_dns_registrar_supported": "YunoHost detectou automáticamente que este dominio está xestionado pola rexistradora **{registrar}**. Se queres, YunoHost pode configurar automáticamente as súas zonas DNS, se proporcionas as credenciais de acceso á API. Podes ver a documentación sobre como obter as credenciais da API nesta páxina: https://yunohost.org/registrar_api_{registrar}. (Tamén podes configurar manualmente os rexistros DNS seguindo a documentación en https://yunohost.org/dns )",
|
||||
"domain_dns_push_partial_failure": "Actualización parcial dos rexistros DNS: informouse dalgúns avisos/erros.",
|
||||
"domain_config_auth_token": "Token de autenticación",
|
||||
"domain_config_auth_key": "Chave de autenticación",
|
||||
"domain_config_auth_entrypoint": "Punto de entrada da API",
|
||||
"domain_dns_push_failed_to_authenticate": "Fallou a autenticación na API da rexistradora do dominio '{domain}'. Comprobaches que sexan as credenciais correctas? (Erro: {error})",
|
||||
"domain_registrar_is_not_configured": "A rexistradora non aínda non está configurada para o dominio {domain}.",
|
||||
"domain_dns_push_not_applicable": "A función de rexistro DNS automático non é aplicable ao dominio {domain}. Debes configurar manualmente os teus rexistros DNS seguindo a documentación de https://yunohost.org/dns_config.",
|
||||
"domain_dns_push_managed_in_parent_domain": "A función de rexistro DNS automático está xestionada polo dominio nai {parent_domain}.",
|
||||
"ldap_attribute_already_exists": "Xa existe o atributo LDAP '{attribute}' con valor '{value}'",
|
||||
"log_domain_config_set": "Actualizar configuración para o dominio '{}'"
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
"backup_output_directory_forbidden": "Escolha um diretório de saída diferente. Backups não podem ser criados nos subdiretórios /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives",
|
||||
"app_already_installed_cant_change_url": "Este aplicativo já está instalado. A URL não pode ser alterada apenas por esta função. Confira em `app changeurl` se está disponível.",
|
||||
"app_already_up_to_date": "{app} já está atualizado",
|
||||
"app_argument_choice_invalid": "Use uma das opções '{choices}' para o argumento '{name}' em vez de '{value}'",
|
||||
"app_argument_choice_invalid": "Escolha um valor válido para o argumento '{name}' : '{value}' não está entre as opções disponíveis ({choices})",
|
||||
"app_argument_invalid": "Escolha um valor válido para o argumento '{name}': {error}",
|
||||
"app_argument_required": "O argumento '{name}' é obrigatório",
|
||||
"app_location_unavailable": "Esta url ou não está disponível ou está em conflito com outra(s) aplicação(ões) já instalada(s):\n{apps}",
|
||||
|
@ -182,7 +182,7 @@
|
|||
"backup_csv_creation_failed": "Não foi possível criar o arquivo CSV necessário para a restauração",
|
||||
"backup_csv_addition_failed": "Não foi possível adicionar os arquivos que estarão no backup ao arquivo CSV",
|
||||
"backup_create_size_estimation": "O arquivo irá conter cerca de {size} de dados.",
|
||||
"backup_couldnt_bind": "Não foi possível vincular {src} ao {dest}",
|
||||
"backup_couldnt_bind": "Não foi possível vincular {src} ao {dest}.",
|
||||
"certmanager_attempt_to_replace_valid_cert": "Você está tentando sobrescrever um certificado bom e válido para o domínio {domain}! (Use --force para prosseguir mesmo assim)",
|
||||
"backup_with_no_restore_script_for_app": "A aplicação {app} não tem um script de restauração, você não será capaz de automaticamente restaurar o backup dessa aplicação.",
|
||||
"backup_with_no_backup_script_for_app": "A aplicação '{app}' não tem um script de backup. Ignorando.",
|
||||
|
@ -191,5 +191,68 @@
|
|||
"backup_running_hooks": "Executando os hooks de backup...",
|
||||
"backup_permission": "Permissão de backup para {app}",
|
||||
"backup_output_symlink_dir_broken": "O diretório de seu arquivo '{path}' é um link simbólico quebrado. Talvez você tenha esquecido de re/montar ou conectar o dispositivo de armazenamento para onde o link aponta.",
|
||||
"backup_output_directory_required": "Você deve especificar um diretório de saída para o backup"
|
||||
}
|
||||
"backup_output_directory_required": "Você deve especificar um diretório de saída para o backup",
|
||||
"diagnosis_description_apps": "Aplicações",
|
||||
"diagnosis_apps_allgood": "Todos os apps instalados respeitam práticas básicas de empacotamento",
|
||||
"diagnosis_apps_issue": "Um problema foi encontrado para o app {app}",
|
||||
"diagnosis_apps_not_in_app_catalog": "Esta aplicação não está no catálogo de aplicações do YunoHost. Se estava no passado e foi removida, você deve considerar desinstalar este app já que ele não mais receberá atualizações e pode comprometer a integridade e segurança do seu sistema.",
|
||||
"diagnosis_apps_broken": "Esta aplicação está atualmente marcada como quebrada no catálogo de apps do YunoHost. Isto pode ser um problema temporário enquanto os mantenedores consertam o problema. Enquanto isso, atualizar este app está desabilitado.",
|
||||
"diagnosis_apps_bad_quality": "Esta aplicação está atualmente marcada como quebrada no catálogo de apps do YunoHost. Isto pode ser um problema temporário enquanto os mantenedores consertam o problema. Enquanto isso, atualizar este app está desabilitado.",
|
||||
"diagnosis_apps_outdated_ynh_requirement": "A versão instalada deste app requer tão somente yunohost >= 2.x, o que tende a indicar que o app não está atualizado com as práticas de empacotamento recomendadas. Você deve considerar seriamente atualizá-lo.",
|
||||
"diagnosis_apps_deprecated_practices": "A versão instalada deste app usa práticas de empacotamento extremamente velhas que não são mais usadas. Você deve considerar seriamente atualizá-lo.",
|
||||
"certmanager_domain_http_not_working": "O domínio {domain} não parece estar acessível por HTTP. Por favor cheque a categoria 'Web' no diagnóstico para mais informações. (Se você sabe o que está fazendo, use '--no-checks' para desativar estas checagens.)",
|
||||
"diagnosis_description_regenconf": "Configurações do sistema",
|
||||
"diagnosis_description_services": "Cheque de status dos serviços",
|
||||
"diagnosis_basesystem_hardware": "A arquitetura hardware do servidor é {virt} {arch}",
|
||||
"diagnosis_description_web": "Web",
|
||||
"diagnosis_basesystem_ynh_single_version": "Versão {package}: {version} ({repo})",
|
||||
"diagnosis_basesystem_ynh_main_version": "O servidor está rodando YunoHost {main_version} ({repo})",
|
||||
"app_config_unable_to_apply": "Falha ao aplicar valores do painel de configuração.",
|
||||
"app_config_unable_to_read": "Falha ao ler valores do painel de configuração.",
|
||||
"config_apply_failed": "Aplicar as novas configuração falhou: {error}",
|
||||
"config_cant_set_value_on_section": "Você não pode setar um único valor na seção de configuração inteira.",
|
||||
"config_validate_time": "Deve ser um horário válido como HH:MM",
|
||||
"config_validate_url": "Deve ser uma URL válida",
|
||||
"config_version_not_supported": "Versões do painel de configuração '{version}' não são suportadas.",
|
||||
"danger": "Perigo:",
|
||||
"diagnosis_basesystem_ynh_inconsistent_versions": "Você está executando versões inconsistentes dos pacotes YunoHost... provavelmente por causa de uma atualização parcial ou que falhou.",
|
||||
"diagnosis_description_basesystem": "Sistema base",
|
||||
"certmanager_cert_signing_failed": "Não foi possível assinar o novo certificado",
|
||||
"certmanager_unable_to_parse_self_CA_name": "Não foi possível processar nome da autoridade de auto-assinatura (arquivo: {file})",
|
||||
"confirm_app_install_warning": "Aviso: Pode ser que essa aplicação funcione, mas ela não está bem integrada ao YunoHost. Algumas funcionalidades como single sign-on e backup/restauração podem não estar disponíveis. Instalar mesmo assim? [{answers}] ",
|
||||
"config_forbidden_keyword": "A palavra chave '{keyword}' é reservada, você não pode criar ou usar um painel de configuração com uma pergunta com esse id.",
|
||||
"config_no_panel": "Painel de configuração não encontrado.",
|
||||
"config_unknown_filter_key": "A chave de filtro '{filter_key}' está incorreta.",
|
||||
"config_validate_color": "Deve ser uma cor RGB hexadecimal válida",
|
||||
"config_validate_date": "Deve ser uma data válida como no formato AAAA-MM-DD",
|
||||
"config_validate_email": "Deve ser um email válido",
|
||||
"diagnosis_basesystem_kernel": "O servidor está rodando Linux kernel {kernel_version}",
|
||||
"diagnosis_cache_still_valid": "(O cache para a categoria de diagnóstico {category} ainda é valido. Não será diagnosticada novamente ainda)",
|
||||
"diagnosis_cant_run_because_of_dep": "Impossível fazer diagnóstico para {category} enquanto ainda existem problemas importantes relacionados a {dep}.",
|
||||
"diagnosis_diskusage_low": "Unidade de armazenamento <code>{mountpoint}</code> (no dispositivo <code>{device}</code>_) tem somente {free} ({free_percent}%) de espaço restante (de {total}). Tenha cuidado.",
|
||||
"diagnosis_description_ip": "Conectividade internet",
|
||||
"diagnosis_description_dnsrecords": "Registros DNS",
|
||||
"diagnosis_description_mail": "Email",
|
||||
"certmanager_domain_not_diagnosed_yet": "Ainda não há resultado de diagnóstico para o domínio {domain}. Por favor re-execute um diagnóstico para as categorias 'Registros DNS' e 'Web' na seção de diagnósticos para checar se o domínio está pronto para o Let's Encrypt. (Ou, se você souber o que está fazendo, use '--no-checks' para desativar estas checagens.)",
|
||||
"diagnosis_basesystem_host": "O Servidor está rodando Debian {debian_version}",
|
||||
"diagnosis_description_systemresources": "Recursos do sistema",
|
||||
"certmanager_acme_not_configured_for_domain": "O challenge ACME não pode ser realizado para {domain} porque o código correspondente na configuração do nginx está ausente... Por favor tenha certeza de que sua configuração do nginx está atualizada executando o comando `yunohost tools regen-conf nginx --dry-run --with-diff`.",
|
||||
"certmanager_attempt_to_renew_nonLE_cert": "O certificado para o domínio '{domain}' não foi emitido pelo Let's Encrypt. Não é possível renová-lo automaticamente!",
|
||||
"certmanager_attempt_to_renew_valid_cert": "O certificado para o domínio '{domain}' não esta prestes a expirar! (Você pode usar --force se saber o que está fazendo)",
|
||||
"certmanager_cannot_read_cert": "Algo de errado aconteceu ao tentar abrir o atual certificado para o domínio {domain} (arquivo: {file}), motivo: {reason}",
|
||||
"certmanager_cert_install_success": "Certificado Let's Encrypt foi instalado para o domínio '{domain}'",
|
||||
"certmanager_cert_install_success_selfsigned": "Certificado autoassinado foi instalado para o domínio '{domain}'",
|
||||
"certmanager_certificate_fetching_or_enabling_failed": "Tentativa de usar o novo certificado para o domínio {domain} não funcionou...",
|
||||
"certmanager_domain_cert_not_selfsigned": "O certificado para o domínio {domain} não é autoassinado. Você tem certeza que quer substituí-lo? (Use '--force' para fazê-lo)",
|
||||
"certmanager_domain_dns_ip_differs_from_public_ip": "O registro de DNS para o domínio '{domain}' é diferente do IP deste servidor. Por favor cheque a categoria 'Registros DNS' (básico) no diagnóstico para mais informações. Se você modificou recentemente o registro 'A', espere um tempo para ele se propagar (alguns serviços de checagem de propagação de DNS estão disponíveis online). (Se você sabe o que está fazendo, use '--no-checks' para desativar estas checagens.)",
|
||||
"certmanager_hit_rate_limit": "Foram emitidos certificados demais para este conjunto de domínios {domain} recentemente. Por favor tente novamente mais tarde. Veja https://letsencrypt.org/docs/rate-limits/ para mais detalhes",
|
||||
"certmanager_no_cert_file": "Não foi possível ler o arquivo de certificado para o domínio {domain} (arquivo: {file})",
|
||||
"certmanager_self_ca_conf_file_not_found": "Não foi possível encontrar o arquivo de configuração para a autoridade de auto-assinatura (arquivo: {file})",
|
||||
"confirm_app_install_danger": "ATENÇÃO! Sabe-se que esta aplicação ainda é experimental (isso se não que explicitamente não funciona)! Você provavelmente NÃO deve instalar ela a não ser que você saiba o que você está fazendo. NENHUM SUPORTE será fornecido se esta aplicação não funcionar ou quebrar o seu sistema... Se você está disposto a tomar esse rico de toda forma, digite '{answers}'",
|
||||
"confirm_app_install_thirdparty": "ATENÇÃO! Essa aplicação não faz parte do catálogo do YunoHost. Instalar aplicações de terceiros pode comprometer a integridade e segurança do seu sistema. Você provavelmente NÃO deve instalá-la a não ser que você saiba o que você está fazendo. NENHUM SUPORTE será fornecido se este app não funcionar ou quebrar seu sistema... Se você está disposto a tomar este risco de toda forma, digite '{answers}'",
|
||||
"diagnosis_description_ports": "Exposição de portas",
|
||||
"diagnosis_basesystem_hardware_model": "O modelo do servidor é {model}",
|
||||
"diagnosis_backports_in_sources_list": "Parece que o apt (o gerenciador de pacotes) está configurado para usar o repositório backport. A não ser que você saiba o que você esteá fazendo, desencorajamos fortemente a instalação de pacotes de backports porque é provável que crie instabilidades ou conflitos no seu sistema.",
|
||||
"certmanager_cert_renew_success": "Certificado Let's Encrypt renovado para o domínio '{domain}'",
|
||||
"certmanager_warning_subdomain_dns_record": "O subdomínio '{subdomain}' não resolve para o mesmo IP que '{domain}'. Algumas funcionalidades não estarão disponíveis até que você conserte isto e regenere o certificado."
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"app_argument_required": "Аргумент '{name}' необхідний",
|
||||
"app_argument_password_no_default": "Помилка під час розбору аргументу пароля '{name}': аргумент пароля не може мати типове значення з причин безпеки",
|
||||
"app_argument_invalid": "Виберіть правильне значення для аргументу '{name}': {error}",
|
||||
"app_argument_choice_invalid": "Використовуйте один з цих варіантів '{choices}' для аргументу '{name}' замість '{value}'",
|
||||
"app_argument_choice_invalid": "Виберіть дійсне значення для аргументу '{name}': '{value}' не є серед доступних варіантів ({choices})",
|
||||
"app_already_up_to_date": "{app} має найостаннішу версію",
|
||||
"app_already_installed_cant_change_url": "Цей застосунок уже встановлено. URL-адреса не може бути змінена тільки цією функцією. Перевірте в `app changeurl`, якщо вона доступна.",
|
||||
"app_already_installed": "{app} уже встановлено",
|
||||
|
@ -482,7 +482,7 @@
|
|||
"diagnosis_domain_expiration_not_found_details": "Відомості WHOIS для домену {domain} не містять даних про строк дії?",
|
||||
"diagnosis_domain_not_found_details": "Домен {domain} не існує в базі даних WHOIS або строк його дії сплив!",
|
||||
"diagnosis_domain_expiration_not_found": "Неможливо перевірити строк дії деяких доменів",
|
||||
"diagnosis_dns_specialusedomain": "Домен {domain} заснований на домені верхнього рівня спеціального призначення (TLD) і тому не очікується, що у нього будуть актуальні записи DNS.",
|
||||
"diagnosis_dns_specialusedomain": "Домен {domain} заснований на домені верхнього рівня спеціального призначення (TLD) такого як .local або .test і тому не очікується, що у нього будуть актуальні записи DNS.",
|
||||
"diagnosis_dns_try_dyndns_update_force": "Конфігурація DNS цього домену повинна автоматично управлятися YunoHost. Якщо це не так, ви можете спробувати примусово оновити її за допомогою команди <cmd>yunohost dyndns update --force</cmd>.",
|
||||
"diagnosis_dns_point_to_doc": "Якщо вам потрібна допомога з налаштування DNS-записів, зверніться до документації на сайті <a href='https://yunohost.org/dns_config'>https://yunohost.org/dns_config</a>.",
|
||||
"diagnosis_dns_discrepancy": "Наступний запис DNS, схоже, не відповідає рекомендованій конфігурації: <br>Тип: <code>{type}</code><br>Назва: <code>{name}</code><br>Поточне значення: <code>{current}</code><br>Очікуване значення: <code>{value}</code>",
|
||||
|
@ -504,7 +504,7 @@
|
|||
"diagnosis_ip_connected_ipv4": "Сервер під'єднаний до Інтернету через IPv4!",
|
||||
"diagnosis_no_cache": "Для категорії «{category}» ще немає кеша діагностики",
|
||||
"diagnosis_failed": "Не вдалося отримати результат діагностики для категорії '{category}': {error}",
|
||||
"diagnosis_everything_ok": "Усе виглядає добре для {category}!",
|
||||
"diagnosis_everything_ok": "Здається, для категорії '{category}' все справно!",
|
||||
"diagnosis_found_warnings": "Знайдено {warnings} пунктів, які можна поліпшити для {category}.",
|
||||
"diagnosis_found_errors_and_warnings": "Знайдено {errors} істотний (і) питання (и) (і {warnings} попередження (я)), що відносяться до {category}!",
|
||||
"diagnosis_found_errors": "Знайдена {errors} важлива проблема (і), пов'язана з {category}!",
|
||||
|
@ -675,5 +675,39 @@
|
|||
"log_app_config_set": "Застосувати конфігурацію до застосунку '{}'",
|
||||
"service_not_reloading_because_conf_broken": "Неможливо перезавантажити/перезапустити службу '{name}', тому що її конфігурацію порушено: {errors}",
|
||||
"app_argument_password_help_optional": "Введіть один пробіл, щоб очистити пароль",
|
||||
"app_argument_password_help_keep": "Натисніть Enter, щоб зберегти поточне значення"
|
||||
}
|
||||
"app_argument_password_help_keep": "Натисніть Enter, щоб зберегти поточне значення",
|
||||
"domain_registrar_is_not_configured": "Реєстратор ще не конфігуровано для домену {domain}.",
|
||||
"domain_dns_push_not_applicable": "Функція автоматичної конфігурації DNS не застосовується до домену {domain}. Вам слід вручну конфігурувати записи DNS відповідно до документації за адресою https://yunohost.org/dns_config.",
|
||||
"domain_dns_registrar_not_supported": "YunoHost не зміг автоматично виявити реєстратора, який обробляє цей домен. Вам слід вручну конфігурувати записи DNS відповідно до документації за адресою https://yunohost.org/dns.",
|
||||
"diagnosis_http_special_use_tld": "Домен {domain} базується на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він буде відкритий за межами локальної мережі.",
|
||||
"domain_dns_push_managed_in_parent_domain": "Функцією автоконфігурації DNS керує батьківський домен {parent_domain}.",
|
||||
"domain_dns_registrar_managed_in_parent_domain": "Цей домен є піддоменом {parent_domain_link}. Конфігурацією реєстратора DNS слід керувати на панелі конфігурації {parent_domain}.",
|
||||
"domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost dyndns update')",
|
||||
"domain_dns_conf_special_use_tld": "Цей домен засновано на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він матиме актуальні записи DNS.",
|
||||
"domain_dns_registrar_supported": "YunoHost автоматично визначив, що цей домен обслуговується реєстратором **{registrar}**. Якщо ви хочете, YunoHost автоматично налаштує цю DNS-зону, якщо ви надасте йому відповідні облікові дані API. Ви можете знайти документацію про те, як отримати реєстраційні дані API на цій сторінці: https://yunohost.org/registar_api_{registrar}. (Ви також можете вручну налаштувати свої DNS-записи, дотримуючись документації на https://yunohost.org/dns)",
|
||||
"domain_dns_registrar_experimental": "Поки що інтерфейс з API **{registrar}** не був належним чином протестований і перевірений спільнотою YunoHost. Підтримка є **дуже експериментальною** - будьте обережні!",
|
||||
"domain_dns_push_success": "Записи DNS оновлено!",
|
||||
"domain_dns_push_failed": "Оновлення записів DNS зазнало невдачі.",
|
||||
"domain_dns_push_partial_failure": "DNS-записи частково оновлено: повідомлялося про деякі попередження/помилки.",
|
||||
"domain_config_mail_in": "Вхідні електронні листи",
|
||||
"domain_config_mail_out": "Вихідні електронні листи",
|
||||
"domain_config_auth_token": "Токен автентифікації",
|
||||
"domain_config_auth_entrypoint": "Точка входу API",
|
||||
"domain_config_auth_consumer_key": "Ключ споживача",
|
||||
"domain_dns_push_failed_to_authenticate": "Неможливо пройти автентифікацію на API реєстратора для домену '{domain}'. Ймовірно, облікові дані недійсні? (Помилка: {error})",
|
||||
"domain_dns_push_failed_to_list": "Не вдалося скласти список поточних записів за допомогою API реєстратора: {error}",
|
||||
"domain_dns_push_record_failed": "Не вдалося виконати дію {action} запису {type}/{name} : {error}",
|
||||
"domain_config_features_disclaimer": "Поки що вмикання/вимикання функцій пошти або XMPP впливає тільки на рекомендовану та автоконфігурацію DNS, але не на конфігурацію системи!",
|
||||
"domain_config_xmpp": "Миттєвий обмін повідомленнями (XMPP)",
|
||||
"domain_config_auth_key": "Ключ автентифікації",
|
||||
"domain_config_auth_secret": "Секрет автентифікації",
|
||||
"domain_config_api_protocol": "API-протокол",
|
||||
"domain_config_auth_application_key": "Ключ застосунку",
|
||||
"domain_config_auth_application_secret": "Таємний ключ застосунку",
|
||||
"log_domain_config_set": "Оновлення конфігурації для домену '{}'",
|
||||
"log_domain_dns_push": "Передавання записів DNS для домену '{}'",
|
||||
"other_available_options": "...і {n} інших доступних опцій, які не показано",
|
||||
"domain_dns_pushing": "Передання записів DNS...",
|
||||
"ldap_attribute_already_exists": "Атрибут LDAP '{attribute}' вже існує зі значенням '{value}'",
|
||||
"domain_dns_push_already_up_to_date": "Записи вже оновлені, нічого не потрібно робити."
|
||||
}
|
||||
|
|
1101
src/yunohost/app.py
1101
src/yunohost/app.py
File diff suppressed because it is too large
Load diff
255
src/yunohost/app_catalog.py
Normal file
255
src/yunohost/app_catalog.py
Normal file
|
@ -0,0 +1,255 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
from moulinette import m18n
|
||||
from moulinette.utils.log import getActionLogger
|
||||
from moulinette.utils.network import download_json
|
||||
from moulinette.utils.filesystem import (
|
||||
read_json,
|
||||
read_yaml,
|
||||
write_to_json,
|
||||
write_to_yaml,
|
||||
mkdir,
|
||||
)
|
||||
|
||||
from yunohost.utils.i18n import _value_for_locale
|
||||
from yunohost.utils.error import YunohostError
|
||||
|
||||
logger = getActionLogger("yunohost.app_catalog")
|
||||
|
||||
APPS_CATALOG_CACHE = "/var/cache/yunohost/repo"
|
||||
APPS_CATALOG_CONF = "/etc/yunohost/apps_catalog.yml"
|
||||
APPS_CATALOG_API_VERSION = 2
|
||||
APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default"
|
||||
|
||||
|
||||
# Old legacy function...
|
||||
def app_fetchlist():
|
||||
logger.warning(
|
||||
"'yunohost app fetchlist' is deprecated. Please use 'yunohost tools update --apps' instead"
|
||||
)
|
||||
from yunohost.tools import tools_update
|
||||
|
||||
tools_update(target="apps")
|
||||
|
||||
|
||||
def app_catalog(full=False, with_categories=False):
|
||||
"""
|
||||
Return a dict of apps available to installation from Yunohost's app catalog
|
||||
"""
|
||||
|
||||
from yunohost.app import _installed_apps, _set_default_ask_questions
|
||||
|
||||
# Get app list from catalog cache
|
||||
catalog = _load_apps_catalog()
|
||||
installed_apps = set(_installed_apps())
|
||||
|
||||
# Trim info for apps if not using --full
|
||||
for app, infos in catalog["apps"].items():
|
||||
infos["installed"] = app in installed_apps
|
||||
|
||||
infos["manifest"]["description"] = _value_for_locale(
|
||||
infos["manifest"]["description"]
|
||||
)
|
||||
|
||||
if not full:
|
||||
catalog["apps"][app] = {
|
||||
"description": infos["manifest"]["description"],
|
||||
"level": infos["level"],
|
||||
}
|
||||
else:
|
||||
infos["manifest"]["arguments"] = _set_default_ask_questions(
|
||||
infos["manifest"].get("arguments", {})
|
||||
)
|
||||
|
||||
# Trim info for categories if not using --full
|
||||
for category in catalog["categories"]:
|
||||
category["title"] = _value_for_locale(category["title"])
|
||||
category["description"] = _value_for_locale(category["description"])
|
||||
for subtags in category.get("subtags", []):
|
||||
subtags["title"] = _value_for_locale(subtags["title"])
|
||||
|
||||
if not full:
|
||||
catalog["categories"] = [
|
||||
{"id": c["id"], "description": c["description"]}
|
||||
for c in catalog["categories"]
|
||||
]
|
||||
|
||||
if not with_categories:
|
||||
return {"apps": catalog["apps"]}
|
||||
else:
|
||||
return {"apps": catalog["apps"], "categories": catalog["categories"]}
|
||||
|
||||
|
||||
def app_search(string):
|
||||
"""
|
||||
Return a dict of apps whose description or name match the search string
|
||||
"""
|
||||
|
||||
# Retrieve a simple dict listing all apps
|
||||
catalog_of_apps = app_catalog()
|
||||
|
||||
# Selecting apps according to a match in app name or description
|
||||
matching_apps = {"apps": {}}
|
||||
for app in catalog_of_apps["apps"].items():
|
||||
if re.search(string, app[0], flags=re.IGNORECASE) or re.search(
|
||||
string, app[1]["description"], flags=re.IGNORECASE
|
||||
):
|
||||
matching_apps["apps"][app[0]] = app[1]
|
||||
|
||||
return matching_apps
|
||||
|
||||
|
||||
def _initialize_apps_catalog_system():
|
||||
"""
|
||||
This function is meant to intialize the apps_catalog system with YunoHost's default app catalog.
|
||||
"""
|
||||
|
||||
default_apps_catalog_list = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL}]
|
||||
|
||||
try:
|
||||
logger.debug(
|
||||
"Initializing apps catalog system with YunoHost's default app list"
|
||||
)
|
||||
write_to_yaml(APPS_CATALOG_CONF, default_apps_catalog_list)
|
||||
except Exception as e:
|
||||
raise YunohostError(
|
||||
"Could not initialize the apps catalog system... : %s" % str(e)
|
||||
)
|
||||
|
||||
logger.success(m18n.n("apps_catalog_init_success"))
|
||||
|
||||
|
||||
def _read_apps_catalog_list():
|
||||
"""
|
||||
Read the json corresponding to the list of apps catalogs
|
||||
"""
|
||||
|
||||
try:
|
||||
list_ = read_yaml(APPS_CATALOG_CONF)
|
||||
# Support the case where file exists but is empty
|
||||
# by returning [] if list_ is None
|
||||
return list_ if list_ else []
|
||||
except Exception as e:
|
||||
raise YunohostError("Could not read the apps_catalog list ... : %s" % str(e))
|
||||
|
||||
|
||||
def _actual_apps_catalog_api_url(base_url):
|
||||
|
||||
return "{base_url}/v{version}/apps.json".format(
|
||||
base_url=base_url, version=APPS_CATALOG_API_VERSION
|
||||
)
|
||||
|
||||
|
||||
def _update_apps_catalog():
|
||||
"""
|
||||
Fetches the json for each apps_catalog and update the cache
|
||||
|
||||
apps_catalog_list is for example :
|
||||
[ {"id": "default", "url": "https://app.yunohost.org/default/"} ]
|
||||
|
||||
Then for each apps_catalog, the actual json URL to be fetched is like :
|
||||
https://app.yunohost.org/default/vX/apps.json
|
||||
|
||||
And store it in :
|
||||
/var/cache/yunohost/repo/default.json
|
||||
"""
|
||||
|
||||
apps_catalog_list = _read_apps_catalog_list()
|
||||
|
||||
logger.info(m18n.n("apps_catalog_updating"))
|
||||
|
||||
# Create cache folder if needed
|
||||
if not os.path.exists(APPS_CATALOG_CACHE):
|
||||
logger.debug("Initialize folder for apps catalog cache")
|
||||
mkdir(APPS_CATALOG_CACHE, mode=0o750, parents=True, uid="root")
|
||||
|
||||
for apps_catalog in apps_catalog_list:
|
||||
apps_catalog_id = apps_catalog["id"]
|
||||
actual_api_url = _actual_apps_catalog_api_url(apps_catalog["url"])
|
||||
|
||||
# Fetch the json
|
||||
try:
|
||||
apps_catalog_content = download_json(actual_api_url)
|
||||
except Exception as e:
|
||||
raise YunohostError(
|
||||
"apps_catalog_failed_to_download",
|
||||
apps_catalog=apps_catalog_id,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
# Remember the apps_catalog api version for later
|
||||
apps_catalog_content["from_api_version"] = APPS_CATALOG_API_VERSION
|
||||
|
||||
# Save the apps_catalog data in the cache
|
||||
cache_file = "{cache_folder}/{list}.json".format(
|
||||
cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id
|
||||
)
|
||||
try:
|
||||
write_to_json(cache_file, apps_catalog_content)
|
||||
except Exception as e:
|
||||
raise YunohostError(
|
||||
"Unable to write cache data for %s apps_catalog : %s"
|
||||
% (apps_catalog_id, str(e))
|
||||
)
|
||||
|
||||
logger.success(m18n.n("apps_catalog_update_success"))
|
||||
|
||||
|
||||
def _load_apps_catalog():
|
||||
"""
|
||||
Read all the apps catalog cache files and build a single dict (merged_catalog)
|
||||
corresponding to all known apps and categories
|
||||
"""
|
||||
|
||||
merged_catalog = {"apps": {}, "categories": []}
|
||||
|
||||
for apps_catalog_id in [L["id"] for L in _read_apps_catalog_list()]:
|
||||
|
||||
# Let's load the json from cache for this catalog
|
||||
cache_file = "{cache_folder}/{list}.json".format(
|
||||
cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id
|
||||
)
|
||||
|
||||
try:
|
||||
apps_catalog_content = (
|
||||
read_json(cache_file) if os.path.exists(cache_file) else None
|
||||
)
|
||||
except Exception as e:
|
||||
raise YunohostError(
|
||||
"Unable to read cache for apps_catalog %s : %s" % (cache_file, e),
|
||||
raw_msg=True,
|
||||
)
|
||||
|
||||
# Check that the version of the data matches version ....
|
||||
# ... otherwise it means we updated yunohost in the meantime
|
||||
# and need to update the cache for everything to be consistent
|
||||
if (
|
||||
not apps_catalog_content
|
||||
or apps_catalog_content.get("from_api_version") != APPS_CATALOG_API_VERSION
|
||||
):
|
||||
logger.info(m18n.n("apps_catalog_obsolete_cache"))
|
||||
_update_apps_catalog()
|
||||
apps_catalog_content = read_json(cache_file)
|
||||
|
||||
del apps_catalog_content["from_api_version"]
|
||||
|
||||
# Add apps from this catalog to the output
|
||||
for app, info in apps_catalog_content["apps"].items():
|
||||
|
||||
# (N.B. : there's a small edge case where multiple apps catalog could be listing the same apps ...
|
||||
# in which case we keep only the first one found)
|
||||
if app in merged_catalog["apps"]:
|
||||
logger.warning(
|
||||
"Duplicate app %s found between apps catalog %s and %s"
|
||||
% (app, apps_catalog_id, merged_catalog["apps"][app]["repository"])
|
||||
)
|
||||
continue
|
||||
|
||||
info["repository"] = apps_catalog_id
|
||||
merged_catalog["apps"][app] = info
|
||||
|
||||
# Annnnd categories
|
||||
merged_catalog["categories"] += apps_catalog_content["categories"]
|
||||
|
||||
return merged_catalog
|
|
@ -49,10 +49,6 @@ from yunohost.app import (
|
|||
app_info,
|
||||
_is_installed,
|
||||
_make_environment_for_app_script,
|
||||
_patch_legacy_helpers,
|
||||
_patch_legacy_php_versions,
|
||||
_patch_legacy_php_versions_in_settings,
|
||||
LEGACY_PHP_VERSION_REPLACEMENTS,
|
||||
_make_tmp_workdir_for_app,
|
||||
)
|
||||
from yunohost.hook import (
|
||||
|
@ -1190,6 +1186,7 @@ class RestoreManager:
|
|||
"""
|
||||
Apply dirty patch to redirect php5 and php7.0 files to php7.3
|
||||
"""
|
||||
from yunohost.utils.legacy import LEGACY_PHP_VERSION_REPLACEMENTS
|
||||
|
||||
backup_csv = os.path.join(self.work_dir, "backup.csv")
|
||||
|
||||
|
@ -1351,6 +1348,11 @@ class RestoreManager:
|
|||
app_instance_name -- (string) The app name to restore (no app with this
|
||||
name should be already install)
|
||||
"""
|
||||
from yunohost.utils.legacy import (
|
||||
_patch_legacy_php_versions,
|
||||
_patch_legacy_php_versions_in_settings,
|
||||
_patch_legacy_helpers,
|
||||
)
|
||||
from yunohost.user import user_group_list
|
||||
from yunohost.permission import (
|
||||
permission_create,
|
||||
|
@ -1485,7 +1487,11 @@ class RestoreManager:
|
|||
logger.debug(m18n.n("restore_running_app_script", app=app_instance_name))
|
||||
|
||||
# Prepare env. var. to pass to script
|
||||
env_dict = _make_environment_for_app_script(app_instance_name)
|
||||
# FIXME : workdir should be a tmp workdir
|
||||
app_workdir = os.path.join(self.work_dir, "apps", app_instance_name, "settings")
|
||||
env_dict = _make_environment_for_app_script(
|
||||
app_instance_name, workdir=app_workdir
|
||||
)
|
||||
env_dict.update(
|
||||
{
|
||||
"YNH_BACKUP_DIR": self.work_dir,
|
||||
|
@ -1493,9 +1499,6 @@ class RestoreManager:
|
|||
"YNH_APP_BACKUP_DIR": os.path.join(
|
||||
self.work_dir, "apps", app_instance_name, "backup"
|
||||
),
|
||||
"YNH_APP_BASEDIR": os.path.join(
|
||||
self.work_dir, "apps", app_instance_name, "settings"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -1532,11 +1535,9 @@ class RestoreManager:
|
|||
remove_script = os.path.join(app_scripts_in_archive, "remove")
|
||||
|
||||
# Setup environment for remove script
|
||||
env_dict_remove = _make_environment_for_app_script(app_instance_name)
|
||||
env_dict_remove["YNH_APP_BASEDIR"] = os.path.join(
|
||||
self.work_dir, "apps", app_instance_name, "settings"
|
||||
env_dict_remove = _make_environment_for_app_script(
|
||||
app_instance_name, workdir=app_workdir
|
||||
)
|
||||
|
||||
remove_operation_logger = OperationLogger(
|
||||
"remove_on_failed_restore",
|
||||
[("app", app_instance_name)],
|
||||
|
|
|
@ -851,14 +851,9 @@ def _backup_current_cert(domain):
|
|||
|
||||
def _check_domain_is_ready_for_ACME(domain):
|
||||
|
||||
dnsrecords = (
|
||||
Diagnoser.get_cached_report(
|
||||
"dnsrecords",
|
||||
item={"domain": domain, "category": "basic"},
|
||||
warn_if_no_cache=False,
|
||||
)
|
||||
or {}
|
||||
)
|
||||
from yunohost.domain import _get_parent_domain_of
|
||||
from yunohost.dns import _get_dns_zone_for_domain
|
||||
|
||||
httpreachable = (
|
||||
Diagnoser.get_cached_report(
|
||||
"web", item={"domain": domain}, warn_if_no_cache=False
|
||||
|
@ -866,16 +861,47 @@ def _check_domain_is_ready_for_ACME(domain):
|
|||
or {}
|
||||
)
|
||||
|
||||
if not dnsrecords or not httpreachable:
|
||||
parent_domain = _get_parent_domain_of(domain)
|
||||
|
||||
dnsrecords = (
|
||||
Diagnoser.get_cached_report(
|
||||
"dnsrecords",
|
||||
item={"domain": parent_domain, "category": "basic"},
|
||||
warn_if_no_cache=False,
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
base_dns_zone = _get_dns_zone_for_domain(domain)
|
||||
record_name = (
|
||||
domain.replace(f".{base_dns_zone}", "") if domain != base_dns_zone else "@"
|
||||
)
|
||||
A_record_status = dnsrecords.get("data").get(f"A:{record_name}")
|
||||
AAAA_record_status = dnsrecords.get("data").get(f"AAAA:{record_name}")
|
||||
|
||||
# Fallback to wildcard in case no result yet for the DNS name?
|
||||
if not A_record_status:
|
||||
A_record_status = dnsrecords.get("data").get("A:*")
|
||||
if not AAAA_record_status:
|
||||
AAAA_record_status = dnsrecords.get("data").get("AAAA:*")
|
||||
|
||||
if (
|
||||
not httpreachable
|
||||
or not dnsrecords.get("data")
|
||||
or (A_record_status, AAAA_record_status) == (None, None)
|
||||
):
|
||||
raise YunohostValidationError(
|
||||
"certmanager_domain_not_diagnosed_yet", domain=domain
|
||||
)
|
||||
|
||||
# Check if IP from DNS matches public IP
|
||||
if not dnsrecords.get("status") in [
|
||||
"SUCCESS",
|
||||
"WARNING",
|
||||
]: # Warning is for missing IPv6 record which ain't critical for ACME
|
||||
# - 'MISSING' for IPv6 ain't critical for ACME
|
||||
# - IPv4 can be None assuming there's at least an IPv6, and viveversa
|
||||
# - (the case where both are None is checked before)
|
||||
if not (
|
||||
A_record_status in [None, "OK"]
|
||||
and AAAA_record_status in [None, "OK", "MISSING"]
|
||||
):
|
||||
raise YunohostValidationError(
|
||||
"certmanager_domain_dns_ip_differs_from_public_ip", domain=domain
|
||||
)
|
||||
|
|
|
@ -4,7 +4,8 @@ from shutil import copy2
|
|||
|
||||
from moulinette.utils.log import getActionLogger
|
||||
|
||||
from yunohost.app import _is_installed, _patch_legacy_php_versions_in_settings
|
||||
from yunohost.app import _is_installed
|
||||
from yunohost.utils.legacy import _patch_legacy_php_versions_in_settings
|
||||
from yunohost.tools import Migration
|
||||
from yunohost.service import _run_service_command
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ from collections import OrderedDict
|
|||
|
||||
from moulinette import m18n, Moulinette
|
||||
from moulinette.utils.log import getActionLogger
|
||||
from moulinette.utils.filesystem import read_file, write_to_file, read_toml
|
||||
from moulinette.utils.filesystem import read_file, write_to_file, read_toml, mkdir
|
||||
|
||||
from yunohost.domain import (
|
||||
domain_list,
|
||||
|
@ -40,8 +40,9 @@ from yunohost.domain import (
|
|||
domain_config_get,
|
||||
_get_domain_settings,
|
||||
_set_domain_settings,
|
||||
_list_subdomains_of,
|
||||
)
|
||||
from yunohost.utils.dns import dig, YNH_DYNDNS_DOMAINS
|
||||
from yunohost.utils.dns import dig, is_yunohost_dyndns_domain, is_special_use_tld
|
||||
from yunohost.utils.error import YunohostValidationError, YunohostError
|
||||
from yunohost.utils.network import get_public_ip
|
||||
from yunohost.log import is_unit_operation
|
||||
|
@ -61,6 +62,9 @@ def domain_dns_suggest(domain):
|
|||
|
||||
"""
|
||||
|
||||
if is_special_use_tld(domain):
|
||||
return m18n.n("domain_dns_conf_special_use_tld")
|
||||
|
||||
_assert_domain_exists(domain)
|
||||
|
||||
dns_conf = _build_dns_conf(domain)
|
||||
|
@ -104,18 +108,6 @@ def domain_dns_suggest(domain):
|
|||
return result
|
||||
|
||||
|
||||
def _list_subdomains_of(parent_domain):
|
||||
|
||||
_assert_domain_exists(parent_domain)
|
||||
|
||||
out = []
|
||||
for domain in domain_list()["domains"]:
|
||||
if domain.endswith(f".{parent_domain}"):
|
||||
out.append(domain)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
|
||||
"""
|
||||
Internal function that will returns a data structure containing the needed
|
||||
|
@ -169,10 +161,7 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
|
|||
# If this is a ynh_dyndns_domain, we're not gonna include all the subdomains in the conf
|
||||
# Because dynette only accept a specific list of name/type
|
||||
# And the wildcard */A already covers the bulk of use cases
|
||||
if any(
|
||||
base_domain.endswith("." + ynh_dyndns_domain)
|
||||
for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS
|
||||
):
|
||||
if is_yunohost_dyndns_domain(base_domain):
|
||||
subdomains = []
|
||||
else:
|
||||
subdomains = _list_subdomains_of(base_domain)
|
||||
|
@ -297,6 +286,12 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False):
|
|||
|
||||
# Defined by custom hooks ships in apps for example ...
|
||||
|
||||
# FIXME : this ain't practical for apps that may want to add
|
||||
# custom dns records for a subdomain ... there's no easy way for
|
||||
# an app to compare the base domain is the parent of the subdomain ?
|
||||
# (On the other hand, in sep 2021, it looks like no app is using
|
||||
# this mechanism...)
|
||||
|
||||
hook_results = hook_callback("custom_dns_rules", args=[base_domain])
|
||||
for hook_name, results in hook_results.items():
|
||||
#
|
||||
|
@ -426,9 +421,14 @@ def _get_dns_zone_for_domain(domain):
|
|||
# First, check if domain is a nohost.me / noho.st / ynh.fr
|
||||
# This is mainly meant to speed up things for "dyndns update"
|
||||
# ... otherwise we end up constantly doing a bunch of dig requests
|
||||
for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS:
|
||||
if domain.endswith("." + ynh_dyndns_domain):
|
||||
return ynh_dyndns_domain
|
||||
if is_yunohost_dyndns_domain(domain):
|
||||
# Keep only foo.nohost.me even if we have subsub.sub.foo.nohost.me
|
||||
return ".".join(domain.rsplit(".", 3)[-3:])
|
||||
|
||||
# Same thing with .local, .test, ... domains
|
||||
if is_special_use_tld(domain):
|
||||
# Keep only foo.local even if we have subsub.sub.foo.local
|
||||
return ".".join(domain.rsplit(".", 2)[-2:])
|
||||
|
||||
# Check cache
|
||||
cache_folder = "/var/cache/yunohost/dns_zones"
|
||||
|
@ -471,7 +471,7 @@ def _get_dns_zone_for_domain(domain):
|
|||
# Check if there's a NS record for that domain
|
||||
answer = dig(parent, rdtype="NS", full_answers=True, resolvers="force_external")
|
||||
if answer[0] == "ok":
|
||||
os.system(f"mkdir -p {cache_folder}")
|
||||
mkdir(cache_folder, parents=True, force=True)
|
||||
write_to_file(cache_file, parent)
|
||||
return parent
|
||||
|
||||
|
@ -520,7 +520,7 @@ def _get_registrar_config_section(domain):
|
|||
|
||||
# TODO big project, integrate yunohost's dynette as a registrar-like provider
|
||||
# TODO big project, integrate other dyndns providers such as netlib.re, or cf the list of dyndns providers supported by cloudron...
|
||||
if dns_zone in YNH_DYNDNS_DOMAINS:
|
||||
if is_yunohost_dyndns_domain(dns_zone):
|
||||
registrar_infos["registrar"] = OrderedDict(
|
||||
{
|
||||
"type": "alert",
|
||||
|
@ -530,6 +530,15 @@ def _get_registrar_config_section(domain):
|
|||
}
|
||||
)
|
||||
return OrderedDict(registrar_infos)
|
||||
elif is_special_use_tld(dns_zone):
|
||||
registrar_infos["registrar"] = OrderedDict(
|
||||
{
|
||||
"type": "alert",
|
||||
"style": "info",
|
||||
"ask": m18n.n("domain_dns_conf_special_use_tld"),
|
||||
"value": None,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
registrar = _relevant_provider_for_domain(dns_zone)[0]
|
||||
|
@ -603,6 +612,10 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge=
|
|||
|
||||
_assert_domain_exists(domain)
|
||||
|
||||
if is_special_use_tld(domain):
|
||||
logger.info(m18n.n("domain_dns_conf_special_use_tld"))
|
||||
return {}
|
||||
|
||||
if not registrar or registrar == "None": # yes it's None as a string
|
||||
raise YunohostValidationError("domain_dns_push_not_applicable", domain=domain)
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ from typing import Dict, Any
|
|||
from moulinette import m18n, Moulinette
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.utils.log import getActionLogger
|
||||
from moulinette.utils.filesystem import write_to_file, read_yaml, write_to_yaml
|
||||
from moulinette.utils.filesystem import write_to_file, read_yaml, write_to_yaml, rm
|
||||
|
||||
from yunohost.app import (
|
||||
app_ssowatconf,
|
||||
|
@ -105,6 +105,33 @@ def _assert_domain_exists(domain):
|
|||
raise YunohostValidationError("domain_name_unknown", domain=domain)
|
||||
|
||||
|
||||
def _list_subdomains_of(parent_domain):
|
||||
|
||||
_assert_domain_exists(parent_domain)
|
||||
|
||||
out = []
|
||||
for domain in domain_list()["domains"]:
|
||||
if domain.endswith(f".{parent_domain}"):
|
||||
out.append(domain)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _get_parent_domain_of(domain):
|
||||
|
||||
_assert_domain_exists(domain)
|
||||
|
||||
if "." not in domain:
|
||||
return domain
|
||||
|
||||
parent_domain = domain.split(".", 1)[-1]
|
||||
if parent_domain not in domain_list()["domains"]:
|
||||
return domain # Domain is its own parent
|
||||
|
||||
else:
|
||||
return _get_parent_domain_of(parent_domain)
|
||||
|
||||
|
||||
@is_unit_operation()
|
||||
def domain_add(operation_logger, domain, dyndns=False):
|
||||
"""
|
||||
|
@ -301,7 +328,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False):
|
|||
]
|
||||
|
||||
for stuff in stuff_to_delete:
|
||||
os.system("rm -rf {stuff}")
|
||||
rm(stuff, force=True, recursive=True)
|
||||
|
||||
# Sometime we have weird issues with the regenconf where some files
|
||||
# appears as manually modified even though they weren't touched ...
|
||||
|
|
|
@ -33,7 +33,7 @@ import subprocess
|
|||
from moulinette import m18n
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.utils.log import getActionLogger
|
||||
from moulinette.utils.filesystem import write_to_file, read_file
|
||||
from moulinette.utils.filesystem import write_to_file, read_file, rm, chown, chmod
|
||||
from moulinette.utils.network import download_json
|
||||
|
||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||
|
@ -152,13 +152,12 @@ def dyndns_subscribe(
|
|||
|
||||
os.system(
|
||||
"cd /etc/yunohost/dyndns && "
|
||||
"dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER %s"
|
||||
% domain
|
||||
)
|
||||
os.system(
|
||||
"chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private"
|
||||
f"dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER {domain}"
|
||||
)
|
||||
|
||||
chmod("/etc/yunohost/dyndns", 0o600, recursive=True)
|
||||
chown("/etc/yunohost/dyndns", "root", recursive=True)
|
||||
|
||||
private_file = glob.glob("/etc/yunohost/dyndns/*%s*.private" % domain)[0]
|
||||
key_file = glob.glob("/etc/yunohost/dyndns/*%s*.key" % domain)[0]
|
||||
with open(key_file) as f:
|
||||
|
@ -175,12 +174,12 @@ def dyndns_subscribe(
|
|||
timeout=30,
|
||||
)
|
||||
except Exception as e:
|
||||
os.system("rm -f %s" % private_file)
|
||||
os.system("rm -f %s" % key_file)
|
||||
rm(private_file, force=True)
|
||||
rm(key_file, force=True)
|
||||
raise YunohostError("dyndns_registration_failed", error=str(e))
|
||||
if r.status_code != 201:
|
||||
os.system("rm -f %s" % private_file)
|
||||
os.system("rm -f %s" % key_file)
|
||||
rm(private_file, force=True)
|
||||
rm(key_file, force=True)
|
||||
try:
|
||||
error = json.loads(r.text)["error"]
|
||||
except Exception:
|
||||
|
|
|
@ -31,7 +31,6 @@ from moulinette import m18n
|
|||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||
from moulinette.utils import process
|
||||
from moulinette.utils.log import getActionLogger
|
||||
from moulinette.utils.text import prependlines
|
||||
|
||||
FIREWALL_FILE = "/etc/yunohost/firewall.yml"
|
||||
UPNP_CRON_JOB = "/etc/cron.d/yunohost-firewall-upnp"
|
||||
|
@ -240,7 +239,7 @@ def firewall_reload(skip_upnp=False):
|
|||
except process.CalledProcessError as e:
|
||||
logger.debug(
|
||||
"iptables seems to be not available, it outputs:\n%s",
|
||||
prependlines(e.output.rstrip(), "> "),
|
||||
e.output.decode().strip(),
|
||||
)
|
||||
logger.warning(m18n.n("iptables_unavailable"))
|
||||
else:
|
||||
|
@ -273,7 +272,7 @@ def firewall_reload(skip_upnp=False):
|
|||
except process.CalledProcessError as e:
|
||||
logger.debug(
|
||||
"ip6tables seems to be not available, it outputs:\n%s",
|
||||
prependlines(e.output.rstrip(), "> "),
|
||||
e.output.decode().strip(),
|
||||
)
|
||||
logger.warning(m18n.n("ip6tables_unavailable"))
|
||||
else:
|
||||
|
@ -526,6 +525,6 @@ def _on_rule_command_error(returncode, cmd, output):
|
|||
'"%s" returned non-zero exit status %d:\n%s',
|
||||
cmd,
|
||||
returncode,
|
||||
prependlines(output.rstrip(), "> "),
|
||||
output.decode().strip(),
|
||||
)
|
||||
return True
|
||||
|
|
|
@ -34,7 +34,7 @@ from importlib import import_module
|
|||
from moulinette import m18n, Moulinette
|
||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||
from moulinette.utils import log
|
||||
from moulinette.utils.filesystem import read_yaml
|
||||
from moulinette.utils.filesystem import read_yaml, cp
|
||||
|
||||
HOOK_FOLDER = "/usr/share/yunohost/hooks/"
|
||||
CUSTOM_HOOK_FOLDER = "/etc/yunohost/hooks.d/"
|
||||
|
@ -60,8 +60,7 @@ def hook_add(app, file):
|
|||
os.makedirs(CUSTOM_HOOK_FOLDER + action)
|
||||
|
||||
finalpath = CUSTOM_HOOK_FOLDER + action + "/" + priority + "-" + app
|
||||
os.system("cp %s %s" % (file, finalpath))
|
||||
os.system("chown -hR admin: %s" % HOOK_FOLDER)
|
||||
cp(file, finalpath)
|
||||
|
||||
return {"hook": finalpath}
|
||||
|
||||
|
|
|
@ -407,7 +407,7 @@ def is_unit_operation(
|
|||
if isinstance(value, IOBase):
|
||||
try:
|
||||
context[field] = value.name
|
||||
except:
|
||||
except Exception:
|
||||
context[field] = "IOBase"
|
||||
operation_logger = OperationLogger(op_key, related_to, args=context)
|
||||
|
||||
|
|
|
@ -474,7 +474,7 @@ def permission_create(
|
|||
protected=protected,
|
||||
sync_perm=sync_perm,
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
permission_delete(permission, force=True)
|
||||
raise
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from moulinette import m18n
|
|||
from moulinette.utils.filesystem import read_json, write_to_json, write_to_yaml
|
||||
|
||||
from yunohost.utils.error import YunohostError
|
||||
from yunohost.app import (
|
||||
from yunohost.app_catalog import (
|
||||
_initialize_apps_catalog_system,
|
||||
_read_apps_catalog_list,
|
||||
_update_apps_catalog,
|
|
@ -41,7 +41,13 @@ def clean():
|
|||
os.system("mkdir -p /etc/ssowat/")
|
||||
app_ssowatconf()
|
||||
|
||||
test_apps = ["break_yo_system", "legacy_app", "legacy_app__2", "full_domain_app"]
|
||||
test_apps = [
|
||||
"break_yo_system",
|
||||
"legacy_app",
|
||||
"legacy_app__2",
|
||||
"full_domain_app",
|
||||
"my_webapp",
|
||||
]
|
||||
|
||||
for test_app in test_apps:
|
||||
|
||||
|
@ -189,6 +195,32 @@ def test_legacy_app_install_main_domain():
|
|||
assert app_is_not_installed(main_domain, "legacy_app")
|
||||
|
||||
|
||||
def test_app_from_catalog():
|
||||
main_domain = _get_maindomain()
|
||||
|
||||
app_install(
|
||||
"my_webapp",
|
||||
args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0",
|
||||
)
|
||||
app_map_ = app_map(raw=True)
|
||||
assert main_domain in app_map_
|
||||
assert "/site" in app_map_[main_domain]
|
||||
assert "id" in app_map_[main_domain]["/site"]
|
||||
assert app_map_[main_domain]["/site"]["id"] == "my_webapp"
|
||||
|
||||
assert app_is_installed(main_domain, "my_webapp")
|
||||
assert app_is_exposed_on_http(main_domain, "/site", "Custom Web App")
|
||||
|
||||
# Try upgrade, should do nothing
|
||||
app_upgrade("my_webapp")
|
||||
# Force upgrade, should upgrade to the same version
|
||||
app_upgrade("my_webapp", force=True)
|
||||
|
||||
app_remove("my_webapp")
|
||||
|
||||
assert app_is_not_installed(main_domain, "my_webapp")
|
||||
|
||||
|
||||
def test_legacy_app_install_secondary_domain(secondary_domain):
|
||||
|
||||
install_legacy_app(secondary_domain, "/legacy")
|
||||
|
|
|
@ -4,7 +4,12 @@ import os
|
|||
from .conftest import get_test_apps_dir
|
||||
|
||||
from yunohost.utils.error import YunohostError
|
||||
from yunohost.app import app_install, app_remove
|
||||
from yunohost.app import (
|
||||
app_install,
|
||||
app_remove,
|
||||
_is_app_repo_url,
|
||||
_parse_app_instance_name,
|
||||
)
|
||||
from yunohost.domain import _get_maindomain, domain_url_available
|
||||
from yunohost.permission import _validate_and_sanitize_permission_url
|
||||
|
||||
|
@ -28,6 +33,56 @@ def teardown_function(function):
|
|||
pass
|
||||
|
||||
|
||||
def test_parse_app_instance_name():
|
||||
|
||||
assert _parse_app_instance_name("yolo") == ("yolo", 1)
|
||||
assert _parse_app_instance_name("yolo1") == ("yolo1", 1)
|
||||
assert _parse_app_instance_name("yolo__0") == ("yolo__0", 1)
|
||||
assert _parse_app_instance_name("yolo__1") == ("yolo", 1)
|
||||
assert _parse_app_instance_name("yolo__23") == ("yolo", 23)
|
||||
assert _parse_app_instance_name("yolo__42__72") == ("yolo__42", 72)
|
||||
assert _parse_app_instance_name("yolo__23qdqsd") == ("yolo__23qdqsd", 1)
|
||||
assert _parse_app_instance_name("yolo__23qdqsd56") == ("yolo__23qdqsd56", 1)
|
||||
|
||||
|
||||
def test_repo_url_definition():
|
||||
assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh")
|
||||
assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh/")
|
||||
assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar123_ynh.git")
|
||||
assert _is_app_repo_url(
|
||||
"https://github.com/YunoHost-Apps/foobar123_ynh/tree/testing"
|
||||
)
|
||||
assert _is_app_repo_url(
|
||||
"https://github.com/YunoHost-Apps/foobar123_ynh/tree/testing/"
|
||||
)
|
||||
assert _is_app_repo_url("https://github.com/YunoHost-Apps/foo-bar-123_ynh")
|
||||
assert _is_app_repo_url("https://github.com/YunoHost-Apps/foo_bar_123_ynh")
|
||||
assert _is_app_repo_url("https://github.com/YunoHost-Apps/FooBar123_ynh")
|
||||
assert _is_app_repo_url("https://github.com/labriqueinternet/vpnclient_ynh")
|
||||
assert _is_app_repo_url("https://framagit.org/YunoHost/apps/nodebb_ynh")
|
||||
assert _is_app_repo_url(
|
||||
"https://framagit.org/YunoHost/apps/nodebb_ynh/-/tree/testing"
|
||||
)
|
||||
assert _is_app_repo_url("https://gitlab.com/yunohost-apps/foobar_ynh")
|
||||
assert _is_app_repo_url("https://code.antopie.org/miraty/qr_ynh")
|
||||
assert _is_app_repo_url(
|
||||
"https://gitlab.domainepublic.net/Neutrinet/neutrinet_ynh/-/tree/unstable"
|
||||
)
|
||||
assert _is_app_repo_url("git@github.com:YunoHost-Apps/foobar_ynh.git")
|
||||
|
||||
assert not _is_app_repo_url("github.com/YunoHost-Apps/foobar_ynh")
|
||||
assert not _is_app_repo_url("http://github.com/YunoHost-Apps/foobar_ynh")
|
||||
assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_wat")
|
||||
assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_ynh_wat")
|
||||
assert not _is_app_repo_url("https://github.com/YunoHost-Apps/foobar/tree/testing")
|
||||
assert not _is_app_repo_url(
|
||||
"https://github.com/YunoHost-Apps/foobar_ynh_wat/tree/testing"
|
||||
)
|
||||
assert not _is_app_repo_url("https://framagit.org/YunoHost/apps/")
|
||||
assert not _is_app_repo_url("https://framagit.org/YunoHost/apps/pwet")
|
||||
assert not _is_app_repo_url("https://framagit.org/YunoHost/apps/pwet_foo")
|
||||
|
||||
|
||||
def test_urlavailable():
|
||||
|
||||
# Except the maindomain/macnuggets to be available
|
||||
|
|
|
@ -34,8 +34,13 @@ def test_get_dns_zone_from_domain_existing():
|
|||
assert (
|
||||
_get_dns_zone_for_domain("non-existing-domain.yunohost.org") == "yunohost.org"
|
||||
)
|
||||
assert _get_dns_zone_for_domain("yolo.nohost.me") == "nohost.me"
|
||||
assert _get_dns_zone_for_domain("foo.yolo.nohost.me") == "nohost.me"
|
||||
assert _get_dns_zone_for_domain("yolo.nohost.me") == "yolo.nohost.me"
|
||||
assert _get_dns_zone_for_domain("foo.yolo.nohost.me") == "yolo.nohost.me"
|
||||
assert _get_dns_zone_for_domain("bar.foo.yolo.nohost.me") == "yolo.nohost.me"
|
||||
|
||||
assert _get_dns_zone_for_domain("yolo.test") == "yolo.test"
|
||||
assert _get_dns_zone_for_domain("foo.yolo.test") == "yolo.test"
|
||||
|
||||
assert _get_dns_zone_for_domain("yolo.tld") == "yolo.tld"
|
||||
assert _get_dns_zone_for_domain("foo.yolo.tld") == "yolo.tld"
|
||||
|
||||
|
|
|
@ -1049,7 +1049,7 @@ def test_permission_app_remove():
|
|||
def test_permission_app_change_url():
|
||||
app_install(
|
||||
os.path.join(get_test_apps_dir(), "permissions_app_ynh"),
|
||||
args="domain=%s&domain_2=%s&path=%s&admin=%s"
|
||||
args="domain=%s&domain_2=%s&path=%s&is_public=1&admin=%s"
|
||||
% (maindomain, other_domains[0], "/urlpermissionapp", "alice"),
|
||||
force=True,
|
||||
)
|
||||
|
@ -1072,7 +1072,7 @@ def test_permission_app_change_url():
|
|||
def test_permission_protection_management_by_helper():
|
||||
app_install(
|
||||
os.path.join(get_test_apps_dir(), "permissions_app_ynh"),
|
||||
args="domain=%s&domain_2=%s&path=%s&admin=%s"
|
||||
args="domain=%s&domain_2=%s&path=%s&is_public=1&admin=%s"
|
||||
% (maindomain, other_domains[0], "/urlpermissionapp", "alice"),
|
||||
force=True,
|
||||
)
|
||||
|
@ -1135,7 +1135,7 @@ def test_permission_legacy_app_propagation_on_ssowat():
|
|||
|
||||
app_install(
|
||||
os.path.join(get_test_apps_dir(), "legacy_app_ynh"),
|
||||
args="domain=%s&domain_2=%s&path=%s"
|
||||
args="domain=%s&domain_2=%s&path=%s&is_public=1"
|
||||
% (maindomain, other_domains[0], "/legacy"),
|
||||
force=True,
|
||||
)
|
||||
|
|
|
@ -15,6 +15,7 @@ from yunohost.utils.config import (
|
|||
PathQuestion,
|
||||
BooleanQuestion,
|
||||
FileQuestion,
|
||||
evaluate_simple_js_expression,
|
||||
)
|
||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||
|
||||
|
@ -2093,3 +2094,96 @@ def test_normalize_path():
|
|||
assert PathQuestion.normalize("/macnuggets/") == "/macnuggets"
|
||||
assert PathQuestion.normalize("macnuggets/") == "/macnuggets"
|
||||
assert PathQuestion.normalize("////macnuggets///") == "/macnuggets"
|
||||
|
||||
|
||||
def test_simple_evaluate():
|
||||
context = {
|
||||
"a1": 1,
|
||||
"b2": 2,
|
||||
"c10": 10,
|
||||
"foo": "bar",
|
||||
"comp": "1>2",
|
||||
"empty": "",
|
||||
"lorem": "Lorem ipsum dolor et si qua met!",
|
||||
"warning": "Warning! This sentence will fail!",
|
||||
"quote": "Je s'apelle Groot",
|
||||
"and_": "&&",
|
||||
"object": {"a": "Security risk"},
|
||||
}
|
||||
supported = {
|
||||
"42": 42,
|
||||
"9.5": 9.5,
|
||||
"'bopbidibopbopbop'": "bopbidibopbopbop",
|
||||
"true": True,
|
||||
"false": False,
|
||||
"null": None,
|
||||
# Math
|
||||
"1 * (2 + 3 * (4 - 3))": 5,
|
||||
"1 * (2 + 3 * (4 - 3)) > 10 - 2 || 3 * 2 > 9 - 2 * 3": True,
|
||||
"(9 - 2) * 3 - 10": 11,
|
||||
"12 - 2 * -2 + (3 - 4) * 3.1": 12.9,
|
||||
"9 / 12 + 12 * 3 - 5": 31.75,
|
||||
"9 / 12 + 12 * (3 - 5)": -23.25,
|
||||
"12 > 13.1": False,
|
||||
"12 < 14": True,
|
||||
"12 <= 14": True,
|
||||
"12 >= 14": False,
|
||||
"12 == 14": False,
|
||||
"12 % 5 > 3": False,
|
||||
"12 != 14": True,
|
||||
"9 - 1 > 10 && 3 * 5 > 10": False,
|
||||
"9 - 1 > 10 || 3 * 5 > 10": True,
|
||||
"a1 > 0 || a1 < -12": True,
|
||||
"a1 > 0 && a1 < -12": False,
|
||||
"a1 + 1 > 0 && -a1 > -12": True,
|
||||
"-(a1 + 1) < 0 || -(a1 + 2) > -12": True,
|
||||
"-a1 * 2": -2,
|
||||
"(9 - 2) * 3 - c10": 11,
|
||||
"(9 - b2) * 3 - c10": 11,
|
||||
"c10 > b2": True,
|
||||
# String
|
||||
"foo == 'bar'": True,
|
||||
"foo != 'bar'": False,
|
||||
'foo == "bar" && 1 > 0': True,
|
||||
"!!foo": True,
|
||||
"!foo": False,
|
||||
"foo": "bar",
|
||||
'!(foo > "baa") || 1 > 2': False,
|
||||
'!(foo > "baa") || 1 < 2': True,
|
||||
'empty == ""': True,
|
||||
'1 == "1"': True,
|
||||
'1.0 == "1"': True,
|
||||
'1 == "aaa"': False,
|
||||
"'I am ' + b2 + ' years'": "I am 2 years",
|
||||
"quote == 'Je s\\'apelle Groot'": True,
|
||||
"lorem == 'Lorem ipsum dolor et si qua met!'": True,
|
||||
"and_ == '&&'": True,
|
||||
"warning == 'Warning! This sentence will fail!'": True,
|
||||
# Match
|
||||
"match(lorem, '^Lorem [ia]psumE?')": bool,
|
||||
"match(foo, '^Lorem [ia]psumE?')": None,
|
||||
"match(lorem, '^Lorem [ia]psumE?') && 1 == 1": bool,
|
||||
# No code
|
||||
"": False,
|
||||
" ": False,
|
||||
}
|
||||
trigger_errors = {
|
||||
"object.a": YunohostError, # Keep unsupported, for security reasons
|
||||
"a1 ** b2": YunohostError, # Keep unsupported, for security reasons
|
||||
"().__class__.__bases__[0].__subclasses__()": YunohostError, # Very dangerous code
|
||||
"a1 > 11 ? 1 : 0": SyntaxError,
|
||||
"c10 > b2 == false": YunohostError, # JS and Python doesn't do the same thing for this situation
|
||||
"c10 > b2 == true": YunohostError,
|
||||
}
|
||||
|
||||
for expression, result in supported.items():
|
||||
if result == bool:
|
||||
assert bool(evaluate_simple_js_expression(expression, context)), expression
|
||||
else:
|
||||
assert (
|
||||
evaluate_simple_js_expression(expression, context) == result
|
||||
), expression
|
||||
|
||||
for expression, error in trigger_errors.items():
|
||||
with pytest.raises(error):
|
||||
evaluate_simple_js_expression(expression, context)
|
||||
|
|
|
@ -34,13 +34,15 @@ from typing import List
|
|||
from moulinette import Moulinette, m18n
|
||||
from moulinette.utils.log import getActionLogger
|
||||
from moulinette.utils.process import check_output, call_async_output
|
||||
from moulinette.utils.filesystem import read_yaml, write_to_yaml
|
||||
from moulinette.utils.filesystem import read_yaml, write_to_yaml, cp, mkdir, rm
|
||||
|
||||
from yunohost.app import (
|
||||
_update_apps_catalog,
|
||||
app_info,
|
||||
app_upgrade,
|
||||
)
|
||||
from yunohost.app_catalog import (
|
||||
_initialize_apps_catalog_system,
|
||||
_update_apps_catalog,
|
||||
)
|
||||
from yunohost.domain import domain_add
|
||||
from yunohost.dyndns import _dyndns_available, _dyndns_provides
|
||||
|
@ -1145,12 +1147,14 @@ class Migration(object):
|
|||
backup_folder = "/home/yunohost.backup/premigration/" + time.strftime(
|
||||
"%Y%m%d-%H%M%S", time.gmtime()
|
||||
)
|
||||
os.makedirs(backup_folder, 0o750)
|
||||
mkdir(backup_folder, 0o750, parents=True)
|
||||
os.system("systemctl stop slapd")
|
||||
os.system(f"cp -r --preserve /etc/ldap {backup_folder}/ldap_config")
|
||||
os.system(f"cp -r --preserve /var/lib/ldap {backup_folder}/ldap_db")
|
||||
os.system(
|
||||
f"cp -r --preserve /etc/yunohost/apps {backup_folder}/apps_settings"
|
||||
cp("/etc/ldap", f"{backup_folder}/ldap_config", recursive=True)
|
||||
cp("/var/lib/ldap", f"{backup_folder}/ldap_db", recursive=True)
|
||||
cp(
|
||||
"/etc/yunohost/apps",
|
||||
f"{backup_folder}/apps_settings",
|
||||
recursive=True,
|
||||
)
|
||||
except Exception as e:
|
||||
raise YunohostError(
|
||||
|
@ -1167,17 +1171,19 @@ class Migration(object):
|
|||
)
|
||||
os.system("systemctl stop slapd")
|
||||
# To be sure that we don't keep some part of the old config
|
||||
os.system("rm -r /etc/ldap/slapd.d")
|
||||
os.system(f"cp -r --preserve {backup_folder}/ldap_config/. /etc/ldap/")
|
||||
os.system(f"cp -r --preserve {backup_folder}/ldap_db/. /var/lib/ldap/")
|
||||
os.system(
|
||||
f"cp -r --preserve {backup_folder}/apps_settings/. /etc/yunohost/apps/"
|
||||
rm("/etc/ldap/slapd.d", force=True, recursive=True)
|
||||
cp(f"{backup_folder}/ldap_config", "/etc/ldap", recursive=True)
|
||||
cp(f"{backup_folder}/ldap_db", "/var/lib/ldap", recursive=True)
|
||||
cp(
|
||||
f"{backup_folder}/apps_settings",
|
||||
"/etc/yunohost/apps",
|
||||
recursive=True,
|
||||
)
|
||||
os.system("systemctl start slapd")
|
||||
os.system(f"rm -r {backup_folder}")
|
||||
rm(backup_folder, force=True, recursive=True)
|
||||
logger.info(m18n.n("migration_ldap_rollback_success"))
|
||||
raise
|
||||
else:
|
||||
os.system(f"rm -r {backup_folder}")
|
||||
rm(backup_folder, force=True, recursive=True)
|
||||
|
||||
return func
|
||||
|
|
|
@ -677,7 +677,7 @@ def user_import(operation_logger, csvfile, update=False, delete=False):
|
|||
|
||||
def to_list(str_list):
|
||||
L = str_list.split(",") if str_list else []
|
||||
L = [l.strip() for l in L]
|
||||
L = [element.strip() for element in L]
|
||||
return L
|
||||
|
||||
existing_users = user_list()["users"]
|
||||
|
|
|
@ -24,6 +24,8 @@ import re
|
|||
import urllib.parse
|
||||
import tempfile
|
||||
import shutil
|
||||
import ast
|
||||
import operator as op
|
||||
from collections import OrderedDict
|
||||
from typing import Optional, Dict, List, Union, Any, Mapping
|
||||
|
||||
|
@ -46,6 +48,145 @@ from yunohost.log import OperationLogger
|
|||
logger = getActionLogger("yunohost.config")
|
||||
CONFIG_PANEL_VERSION_SUPPORTED = 1.0
|
||||
|
||||
# Those js-like evaluate functions are used to eval safely visible attributes
|
||||
# The goal is to evaluate in the same way than js simple-evaluate
|
||||
# https://github.com/shepherdwind/simple-evaluate
|
||||
def evaluate_simple_ast(node, context={}):
|
||||
operators = {
|
||||
ast.Not: op.not_,
|
||||
ast.Mult: op.mul,
|
||||
ast.Div: op.truediv, # number
|
||||
ast.Mod: op.mod, # number
|
||||
ast.Add: op.add, # str
|
||||
ast.Sub: op.sub, # number
|
||||
ast.USub: op.neg, # Negative number
|
||||
ast.Gt: op.gt,
|
||||
ast.Lt: op.lt,
|
||||
ast.GtE: op.ge,
|
||||
ast.LtE: op.le,
|
||||
ast.Eq: op.eq,
|
||||
ast.NotEq: op.ne,
|
||||
}
|
||||
context["true"] = True
|
||||
context["false"] = False
|
||||
context["null"] = None
|
||||
|
||||
# Variable
|
||||
if isinstance(node, ast.Name): # Variable
|
||||
return context[node.id]
|
||||
|
||||
# Python <=3.7 String
|
||||
elif isinstance(node, ast.Str):
|
||||
return node.s
|
||||
|
||||
# Python <=3.7 Number
|
||||
elif isinstance(node, ast.Num):
|
||||
return node.n
|
||||
|
||||
# Boolean, None and Python 3.8 for Number, Boolean, String and None
|
||||
elif isinstance(node, (ast.Constant, ast.NameConstant)):
|
||||
return node.value
|
||||
|
||||
# + - * / %
|
||||
elif (
|
||||
isinstance(node, ast.BinOp) and type(node.op) in operators
|
||||
): # <left> <operator> <right>
|
||||
left = evaluate_simple_ast(node.left, context)
|
||||
right = evaluate_simple_ast(node.right, context)
|
||||
if type(node.op) == ast.Add:
|
||||
if isinstance(left, str) or isinstance(right, str): # support 'I am ' + 42
|
||||
left = str(left)
|
||||
right = str(right)
|
||||
elif type(left) != type(right): # support "111" - "1" -> 110
|
||||
left = float(left)
|
||||
right = float(right)
|
||||
|
||||
return operators[type(node.op)](left, right)
|
||||
|
||||
# Comparison
|
||||
# JS and Python don't give the same result for multi operators
|
||||
# like True == 10 > 2.
|
||||
elif (
|
||||
isinstance(node, ast.Compare) and len(node.comparators) == 1
|
||||
): # <left> <ops> <comparators>
|
||||
left = evaluate_simple_ast(node.left, context)
|
||||
right = evaluate_simple_ast(node.comparators[0], context)
|
||||
operator = node.ops[0]
|
||||
if isinstance(left, (int, float)) or isinstance(right, (int, float)):
|
||||
try:
|
||||
left = float(left)
|
||||
right = float(right)
|
||||
except ValueError:
|
||||
return type(operator) == ast.NotEq
|
||||
try:
|
||||
return operators[type(operator)](left, right)
|
||||
except TypeError: # support "e" > 1 -> False like in JS
|
||||
return False
|
||||
|
||||
# and / or
|
||||
elif isinstance(node, ast.BoolOp): # <op> <values>
|
||||
for value in node.values:
|
||||
value = evaluate_simple_ast(value, context)
|
||||
if isinstance(node.op, ast.And) and not value:
|
||||
return False
|
||||
elif isinstance(node.op, ast.Or) and value:
|
||||
return True
|
||||
return isinstance(node.op, ast.And)
|
||||
|
||||
# not / USub (it's negation number -\d)
|
||||
elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
|
||||
return operators[type(node.op)](evaluate_simple_ast(node.operand, context))
|
||||
|
||||
# match function call
|
||||
elif isinstance(node, ast.Call) and node.func.__dict__.get("id") == "match":
|
||||
return re.match(
|
||||
evaluate_simple_ast(node.args[1], context), context[node.args[0].id]
|
||||
)
|
||||
|
||||
# Unauthorized opcode
|
||||
else:
|
||||
opcode = str(type(node))
|
||||
raise YunohostError(
|
||||
f"Unauthorize opcode '{opcode}' in visible attribute", raw_msg=True
|
||||
)
|
||||
|
||||
|
||||
def js_to_python(expr):
|
||||
in_string = None
|
||||
py_expr = ""
|
||||
i = 0
|
||||
escaped = False
|
||||
for char in expr:
|
||||
if char in r"\"'":
|
||||
# Start a string
|
||||
if not in_string:
|
||||
in_string = char
|
||||
|
||||
# Finish a string
|
||||
elif in_string == char and not escaped:
|
||||
in_string = None
|
||||
|
||||
# If we are not in a string, replace operators
|
||||
elif not in_string:
|
||||
if char == "!" and expr[i + 1] != "=":
|
||||
char = "not "
|
||||
elif char in "|&" and py_expr[-1:] == char:
|
||||
py_expr = py_expr[:-1]
|
||||
char = " and " if char == "&" else " or "
|
||||
|
||||
# Determine if next loop will be in escaped mode
|
||||
escaped = char == "\\" and not escaped
|
||||
py_expr += char
|
||||
i += 1
|
||||
return py_expr
|
||||
|
||||
|
||||
def evaluate_simple_js_expression(expr, context={}):
|
||||
if not expr.strip():
|
||||
return False
|
||||
node = ast.parse(js_to_python(expr), mode="eval").body
|
||||
return evaluate_simple_ast(node, context)
|
||||
|
||||
|
||||
class ConfigPanel:
|
||||
def __init__(self, config_path, save_path=None):
|
||||
|
@ -469,11 +610,13 @@ class Question(object):
|
|||
hide_user_input_in_prompt = False
|
||||
pattern: Optional[Dict] = None
|
||||
|
||||
def __init__(self, question: Dict[str, Any]):
|
||||
def __init__(self, question: Dict[str, Any], context: Mapping[str, Any] = {}):
|
||||
self.name = question["name"]
|
||||
self.type = question.get("type", "string")
|
||||
self.default = question.get("default", None)
|
||||
self.optional = question.get("optional", False)
|
||||
self.visible = question.get("visible", None)
|
||||
self.context = context
|
||||
self.choices = question.get("choices", [])
|
||||
self.pattern = question.get("pattern", self.pattern)
|
||||
self.ask = question.get("ask", {"en": self.name})
|
||||
|
@ -515,6 +658,17 @@ class Question(object):
|
|||
)
|
||||
|
||||
def ask_if_needed(self):
|
||||
|
||||
if self.visible and not evaluate_simple_js_expression(
|
||||
self.visible, context=self.context
|
||||
):
|
||||
# FIXME There could be several use case if the question is not displayed:
|
||||
# - we doesn't want to give a specific value
|
||||
# - we want to keep the previous value
|
||||
# - we want the default value
|
||||
self.value = None
|
||||
return self.value
|
||||
|
||||
for i in range(5):
|
||||
# Display question if no value filled or if it's a readonly message
|
||||
if Moulinette.interface.type == "cli" and os.isatty(1):
|
||||
|
@ -580,7 +734,7 @@ class Question(object):
|
|||
# Prevent displaying a shitload of choices
|
||||
# (e.g. 100+ available users when choosing an app admin...)
|
||||
choices = (
|
||||
list(self.choices.values())
|
||||
list(self.choices.keys())
|
||||
if isinstance(self.choices, dict)
|
||||
else self.choices
|
||||
)
|
||||
|
@ -713,8 +867,8 @@ class PasswordQuestion(Question):
|
|||
default_value = ""
|
||||
forbidden_chars = "{}"
|
||||
|
||||
def __init__(self, question):
|
||||
super().__init__(question)
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
super().__init__(question, context)
|
||||
self.redact = True
|
||||
if self.default is not None:
|
||||
raise YunohostValidationError(
|
||||
|
@ -832,8 +986,8 @@ class BooleanQuestion(Question):
|
|||
choices="yes/no",
|
||||
)
|
||||
|
||||
def __init__(self, question):
|
||||
super().__init__(question)
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
super().__init__(question, context)
|
||||
self.yes = question.get("yes", 1)
|
||||
self.no = question.get("no", 0)
|
||||
if self.default is None:
|
||||
|
@ -853,10 +1007,10 @@ class BooleanQuestion(Question):
|
|||
class DomainQuestion(Question):
|
||||
argument_type = "domain"
|
||||
|
||||
def __init__(self, question):
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
from yunohost.domain import domain_list, _get_maindomain
|
||||
|
||||
super().__init__(question)
|
||||
super().__init__(question, context)
|
||||
|
||||
if self.default is None:
|
||||
self.default = _get_maindomain()
|
||||
|
@ -879,11 +1033,11 @@ class DomainQuestion(Question):
|
|||
class UserQuestion(Question):
|
||||
argument_type = "user"
|
||||
|
||||
def __init__(self, question):
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
from yunohost.user import user_list, user_info
|
||||
from yunohost.domain import _get_maindomain
|
||||
|
||||
super().__init__(question)
|
||||
super().__init__(question, context)
|
||||
self.choices = list(user_list()["users"].keys())
|
||||
|
||||
if not self.choices:
|
||||
|
@ -905,8 +1059,8 @@ class NumberQuestion(Question):
|
|||
argument_type = "number"
|
||||
default_value = None
|
||||
|
||||
def __init__(self, question):
|
||||
super().__init__(question)
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
super().__init__(question, context)
|
||||
self.min = question.get("min", None)
|
||||
self.max = question.get("max", None)
|
||||
self.step = question.get("step", None)
|
||||
|
@ -957,8 +1111,8 @@ class DisplayTextQuestion(Question):
|
|||
argument_type = "display_text"
|
||||
readonly = True
|
||||
|
||||
def __init__(self, question):
|
||||
super().__init__(question)
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
super().__init__(question, context)
|
||||
|
||||
self.optional = True
|
||||
self.style = question.get(
|
||||
|
@ -992,8 +1146,8 @@ class FileQuestion(Question):
|
|||
if os.path.exists(upload_dir):
|
||||
shutil.rmtree(upload_dir)
|
||||
|
||||
def __init__(self, question):
|
||||
super().__init__(question)
|
||||
def __init__(self, question, context: Mapping[str, Any] = {}):
|
||||
super().__init__(question, context)
|
||||
self.accept = question.get("accept", "")
|
||||
|
||||
def _prevalidate(self):
|
||||
|
@ -1022,10 +1176,13 @@ class FileQuestion(Question):
|
|||
FileQuestion.upload_dirs += [upload_dir]
|
||||
|
||||
logger.debug(f"Saving file {self.name} for file question into {file_path}")
|
||||
if Moulinette.interface.type != "api":
|
||||
content = read_file(str(self.value), file_mode="rb")
|
||||
|
||||
if Moulinette.interface.type == "api":
|
||||
def is_file_path(s):
|
||||
return isinstance(s, str) and s.startswith("/") and os.path.exists(s)
|
||||
|
||||
if Moulinette.interface.type != "api" or is_file_path(self.value):
|
||||
content = read_file(str(self.value), file_mode="rb")
|
||||
else:
|
||||
content = b64decode(self.value)
|
||||
|
||||
write_to_file(file_path, content, file_mode="wb")
|
||||
|
@ -1060,15 +1217,15 @@ ARGUMENTS_TYPE_PARSERS = {
|
|||
|
||||
|
||||
def ask_questions_and_parse_answers(
|
||||
questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {}
|
||||
raw_questions: Dict, prefilled_answers: Union[str, Mapping[str, Any]] = {}
|
||||
) -> List[Question]:
|
||||
"""Parse arguments store in either manifest.json or actions.json or from a
|
||||
config panel against the user answers when they are present.
|
||||
|
||||
Keyword arguments:
|
||||
questions -- the arguments description store in yunohost
|
||||
format from actions.json/toml, manifest.json/toml
|
||||
or config_panel.json/toml
|
||||
raw_questions -- the arguments description store in yunohost
|
||||
format from actions.json/toml, manifest.json/toml
|
||||
or config_panel.json/toml
|
||||
prefilled_answers -- a url "query-string" such as "domain=yolo.test&path=/foobar&admin=sam"
|
||||
or a dict such as {"domain": "yolo.test", "path": "/foobar", "admin": "sam"}
|
||||
"""
|
||||
|
@ -1079,21 +1236,22 @@ def ask_questions_and_parse_answers(
|
|||
# whereas parse.qs return list of values (which is useful for tags, etc)
|
||||
# For now, let's not migrate this piece of code to parse_qs
|
||||
# Because Aleks believes some bits of the app CI rely on overriding values (e.g. foo=foo&...&foo=bar)
|
||||
prefilled_answers = dict(
|
||||
answers = dict(
|
||||
urllib.parse.parse_qsl(prefilled_answers or "", keep_blank_values=True)
|
||||
)
|
||||
elif isinstance(prefilled_answers, Mapping):
|
||||
answers = {**prefilled_answers}
|
||||
else:
|
||||
answers = {}
|
||||
|
||||
if not prefilled_answers:
|
||||
prefilled_answers = {}
|
||||
|
||||
out = []
|
||||
|
||||
for question in questions:
|
||||
question_class = ARGUMENTS_TYPE_PARSERS[question.get("type", "string")]
|
||||
question["value"] = prefilled_answers.get(question["name"])
|
||||
question = question_class(question)
|
||||
|
||||
question.ask_if_needed()
|
||||
for raw_question in raw_questions:
|
||||
question_class = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")]
|
||||
raw_question["value"] = answers.get(raw_question["name"])
|
||||
question = question_class(raw_question, context=answers)
|
||||
answers[question.name] = question.ask_if_needed()
|
||||
out.append(question)
|
||||
|
||||
return out
|
||||
|
|
|
@ -23,6 +23,8 @@ from typing import List
|
|||
|
||||
from moulinette.utils.filesystem import read_file
|
||||
|
||||
SPECIAL_USE_TLDS = ["local", "localhost", "onion", "test"]
|
||||
|
||||
YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"]
|
||||
|
||||
# Lazy dev caching to avoid re-reading the file multiple time when calling
|
||||
|
@ -30,6 +32,18 @@ YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"]
|
|||
external_resolvers_: List[str] = []
|
||||
|
||||
|
||||
def is_yunohost_dyndns_domain(domain):
|
||||
|
||||
return any(
|
||||
domain.endswith(f".{dyndns_domain}") for dyndns_domain in YNH_DYNDNS_DOMAINS
|
||||
)
|
||||
|
||||
|
||||
def is_special_use_tld(domain):
|
||||
|
||||
return any(domain.endswith(f".{tld}") for tld in SPECIAL_USE_TLDS)
|
||||
|
||||
|
||||
def external_resolvers():
|
||||
|
||||
global external_resolvers_
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
import os
|
||||
import re
|
||||
import glob
|
||||
from moulinette import m18n
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.utils.log import getActionLogger
|
||||
from moulinette.utils.filesystem import write_to_json, read_yaml
|
||||
from moulinette.utils.filesystem import (
|
||||
read_file,
|
||||
write_to_file,
|
||||
write_to_json,
|
||||
write_to_yaml,
|
||||
read_yaml,
|
||||
)
|
||||
|
||||
from yunohost.user import user_list
|
||||
from yunohost.app import (
|
||||
|
@ -14,6 +23,8 @@ from yunohost.permission import (
|
|||
user_permission_update,
|
||||
permission_sync_to_user,
|
||||
)
|
||||
from yunohost.utils.error import YunohostValidationError
|
||||
|
||||
|
||||
logger = getActionLogger("yunohost.legacy")
|
||||
|
||||
|
@ -237,3 +248,213 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent():
|
|||
logger.warning(
|
||||
"YunoHost automatically translated some legacy rules in /etc/ssowat/conf.json.persistent to match the new permission system"
|
||||
)
|
||||
|
||||
|
||||
LEGACY_PHP_VERSION_REPLACEMENTS = [
|
||||
("/etc/php5", "/etc/php/7.3"),
|
||||
("/etc/php/7.0", "/etc/php/7.3"),
|
||||
("/var/run/php5-fpm", "/var/run/php/php7.3-fpm"),
|
||||
("/var/run/php/php7.0-fpm", "/var/run/php/php7.3-fpm"),
|
||||
("php5", "php7.3"),
|
||||
("php7.0", "php7.3"),
|
||||
(
|
||||
'phpversion="${phpversion:-7.0}"',
|
||||
'phpversion="${phpversion:-7.3}"',
|
||||
), # Many helpers like the composer ones use 7.0 by default ...
|
||||
(
|
||||
'"$phpversion" == "7.0"',
|
||||
'$(bc <<< "$phpversion >= 7.3") -eq 1',
|
||||
), # patch ynh_install_php to refuse installing/removing php <= 7.3
|
||||
]
|
||||
|
||||
|
||||
def _patch_legacy_php_versions(app_folder):
|
||||
|
||||
files_to_patch = []
|
||||
files_to_patch.extend(glob.glob("%s/conf/*" % app_folder))
|
||||
files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder))
|
||||
files_to_patch.extend(glob.glob("%s/scripts/*/*" % app_folder))
|
||||
files_to_patch.extend(glob.glob("%s/scripts/.*" % app_folder))
|
||||
files_to_patch.append("%s/manifest.json" % app_folder)
|
||||
files_to_patch.append("%s/manifest.toml" % app_folder)
|
||||
|
||||
for filename in files_to_patch:
|
||||
|
||||
# Ignore non-regular files
|
||||
if not os.path.isfile(filename):
|
||||
continue
|
||||
|
||||
c = (
|
||||
"sed -i "
|
||||
+ "".join(
|
||||
"-e 's@{pattern}@{replace}@g' ".format(pattern=p, replace=r)
|
||||
for p, r in LEGACY_PHP_VERSION_REPLACEMENTS
|
||||
)
|
||||
+ "%s" % filename
|
||||
)
|
||||
os.system(c)
|
||||
|
||||
|
||||
def _patch_legacy_php_versions_in_settings(app_folder):
|
||||
|
||||
settings = read_yaml(os.path.join(app_folder, "settings.yml"))
|
||||
|
||||
if settings.get("fpm_config_dir") == "/etc/php/7.0/fpm":
|
||||
settings["fpm_config_dir"] = "/etc/php/7.3/fpm"
|
||||
if settings.get("fpm_service") == "php7.0-fpm":
|
||||
settings["fpm_service"] = "php7.3-fpm"
|
||||
if settings.get("phpversion") == "7.0":
|
||||
settings["phpversion"] = "7.3"
|
||||
|
||||
# We delete these checksums otherwise the file will appear as manually modified
|
||||
list_to_remove = ["checksum__etc_php_7.0_fpm_pool", "checksum__etc_nginx_conf.d"]
|
||||
settings = {
|
||||
k: v
|
||||
for k, v in settings.items()
|
||||
if not any(k.startswith(to_remove) for to_remove in list_to_remove)
|
||||
}
|
||||
|
||||
write_to_yaml(app_folder + "/settings.yml", settings)
|
||||
|
||||
|
||||
def _patch_legacy_helpers(app_folder):
|
||||
|
||||
files_to_patch = []
|
||||
files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder))
|
||||
files_to_patch.extend(glob.glob("%s/scripts/.*" % app_folder))
|
||||
|
||||
stuff_to_replace = {
|
||||
# Replace
|
||||
# sudo yunohost app initdb $db_user -p $db_pwd
|
||||
# by
|
||||
# ynh_mysql_setup_db --db_user=$db_user --db_name=$db_user --db_pwd=$db_pwd
|
||||
"yunohost app initdb": {
|
||||
"pattern": r"(sudo )?yunohost app initdb \"?(\$\{?\w+\}?)\"?\s+-p\s\"?(\$\{?\w+\}?)\"?",
|
||||
"replace": r"ynh_mysql_setup_db --db_user=\2 --db_name=\2 --db_pwd=\3",
|
||||
"important": True,
|
||||
},
|
||||
# Replace
|
||||
# sudo yunohost app checkport whaterver
|
||||
# by
|
||||
# ynh_port_available whatever
|
||||
"yunohost app checkport": {
|
||||
"pattern": r"(sudo )?yunohost app checkport",
|
||||
"replace": r"ynh_port_available",
|
||||
"important": True,
|
||||
},
|
||||
# We can't migrate easily port-available
|
||||
# .. but at the time of writing this code, only two non-working apps are using it.
|
||||
"yunohost tools port-available": {"important": True},
|
||||
# Replace
|
||||
# yunohost app checkurl "${domain}${path_url}" -a "${app}"
|
||||
# by
|
||||
# ynh_webpath_register --app=${app} --domain=${domain} --path_url=${path_url}
|
||||
"yunohost app checkurl": {
|
||||
"pattern": r"(sudo )?yunohost app checkurl \"?(\$\{?\w+\}?)\/?(\$\{?\w+\}?)\"?\s+-a\s\"?(\$\{?\w+\}?)\"?",
|
||||
"replace": r"ynh_webpath_register --app=\4 --domain=\2 --path_url=\3",
|
||||
"important": True,
|
||||
},
|
||||
# Remove
|
||||
# Automatic diagnosis data from YunoHost
|
||||
# __PRE_TAG1__$(yunohost tools diagnosis | ...)__PRE_TAG2__"
|
||||
#
|
||||
"yunohost tools diagnosis": {
|
||||
"pattern": r"(Automatic diagnosis data from YunoHost( *\n)*)? *(__\w+__)? *\$\(yunohost tools diagnosis.*\)(__\w+__)?",
|
||||
"replace": r"",
|
||||
"important": False,
|
||||
},
|
||||
# Old $1, $2 in backup/restore scripts...
|
||||
"app=$2": {
|
||||
"only_for": ["scripts/backup", "scripts/restore"],
|
||||
"pattern": r"app=\$2",
|
||||
"replace": r"app=$YNH_APP_INSTANCE_NAME",
|
||||
"important": True,
|
||||
},
|
||||
# Old $1, $2 in backup/restore scripts...
|
||||
"backup_dir=$1": {
|
||||
"only_for": ["scripts/backup", "scripts/restore"],
|
||||
"pattern": r"backup_dir=\$1",
|
||||
"replace": r"backup_dir=.",
|
||||
"important": True,
|
||||
},
|
||||
# Old $1, $2 in backup/restore scripts...
|
||||
"restore_dir=$1": {
|
||||
"only_for": ["scripts/restore"],
|
||||
"pattern": r"restore_dir=\$1",
|
||||
"replace": r"restore_dir=.",
|
||||
"important": True,
|
||||
},
|
||||
# Old $1, $2 in install scripts...
|
||||
# We ain't patching that shit because it ain't trivial to patch all args...
|
||||
"domain=$1": {"only_for": ["scripts/install"], "important": True},
|
||||
}
|
||||
|
||||
for helper, infos in stuff_to_replace.items():
|
||||
infos["pattern"] = (
|
||||
re.compile(infos["pattern"]) if infos.get("pattern") else None
|
||||
)
|
||||
infos["replace"] = infos.get("replace")
|
||||
|
||||
for filename in files_to_patch:
|
||||
|
||||
# Ignore non-regular files
|
||||
if not os.path.isfile(filename):
|
||||
continue
|
||||
|
||||
try:
|
||||
content = read_file(filename)
|
||||
except MoulinetteError:
|
||||
continue
|
||||
|
||||
replaced_stuff = False
|
||||
show_warning = False
|
||||
|
||||
for helper, infos in stuff_to_replace.items():
|
||||
|
||||
# Ignore if not relevant for this file
|
||||
if infos.get("only_for") and not any(
|
||||
filename.endswith(f) for f in infos["only_for"]
|
||||
):
|
||||
continue
|
||||
|
||||
# If helper is used, attempt to patch the file
|
||||
if helper in content and infos["pattern"]:
|
||||
content = infos["pattern"].sub(infos["replace"], content)
|
||||
replaced_stuff = True
|
||||
if infos["important"]:
|
||||
show_warning = True
|
||||
|
||||
# If the helper is *still* in the content, it means that we
|
||||
# couldn't patch the deprecated helper in the previous lines. In
|
||||
# that case, abort the install or whichever step is performed
|
||||
if helper in content and infos["important"]:
|
||||
raise YunohostValidationError(
|
||||
"This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.",
|
||||
raw_msg=True,
|
||||
)
|
||||
|
||||
if replaced_stuff:
|
||||
|
||||
# Check the app do load the helper
|
||||
# If it doesn't, add the instruction ourselve (making sure it's after the #!/bin/bash if it's there...
|
||||
if filename.split("/")[-1] in [
|
||||
"install",
|
||||
"remove",
|
||||
"upgrade",
|
||||
"backup",
|
||||
"restore",
|
||||
]:
|
||||
source_helpers = "source /usr/share/yunohost/helpers"
|
||||
if source_helpers not in content:
|
||||
content.replace("#!/bin/bash", "#!/bin/bash\n" + source_helpers)
|
||||
if source_helpers not in content:
|
||||
content = source_helpers + "\n" + content
|
||||
|
||||
# Actually write the new content in the file
|
||||
write_to_file(filename, content)
|
||||
|
||||
if show_warning:
|
||||
# And complain about those damn deprecated helpers
|
||||
logger.error(
|
||||
r"/!\ Packagers ! This app uses a very old deprecated helpers ... Yunohost automatically patched the helpers to use the new recommended practice, but please do consider fixing the upstream code right now ..."
|
||||
)
|
||||
|
|
2
tox.ini
2
tox.ini
|
@ -9,7 +9,7 @@ deps =
|
|||
py37-mypy: mypy >= 0.900
|
||||
commands =
|
||||
py37-lint: flake8 src doc data tests --ignore E402,E501,E203,W503 --exclude src/yunohost/vendor
|
||||
py37-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F
|
||||
py37-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F,E722,W605
|
||||
py37-black-check: black --check --diff src doc data tests
|
||||
py37-black-run: black src doc data tests
|
||||
py37-mypy: mypy --ignore-missing-import --install-types --non-interactive --follow-imports silent src/yunohost/ --exclude (acme_tiny|data_migrations)
|
||||
|
|
Loading…
Add table
Reference in a new issue