diff --git a/prefsCleaner.sh b/prefsCleaner.sh index b9739b2c..b38ce6b8 100755 --- a/prefsCleaner.sh +++ b/prefsCleaner.sh @@ -1,185 +1,1129 @@ -#!/usr/bin/env bash +#!/bin/sh -## prefs.js cleaner for Linux/Mac -## author: @claustromaniac -## version: 2.1 +# prefs.js cleaner for macOS, Linux and Unix-like operating systems +# authors: @claustromaniac, @earthlng, @9ao9ai9ar +# version: 3.0 -## special thanks to @overdodactyl and @earthlng for a few snippets that I stol..*cough* borrowed from the updater.sh +# IMPORTANT: The version string must be on the 5th line of this file +# and must be of the format "version: MAJOR.MINOR" (spaces are optional). +# This restriction is set by the function arkenfox_script_version. -## DON'T GO HIGHER THAN VERSION x.9 !! ( because of ASCII comparison in update_prefsCleaner() ) +# Example advanced script usage: +# $ yes | tr -d '\n' | env WGET__IMPLEMENTATION=wget ./prefsCleaner.sh 2>/dev/null +# $ TERM=dumb . ./prefsCleaner.sh && arkenfox_prefs_cleaner -readonly CURRDIR=$(pwd) +# This ShellCheck warning is just noise for those who know what they are doing: +# "Note that A && B || C is not if-then-else. C may run when A is true." +# shellcheck disable=SC2015 -## get the full path of this script (readlink for Linux, greadlink for Mac with coreutils installed) -SCRIPT_FILE=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null) +############################################################################### +#### === Common utility functions === #### +#### Code that is shared between updater.sh and prefsCleaner.sh, inlined #### +#### and duplicated only to maintain the same file count as before. #### +############################################################################### -## fallback for Macs without coreutils -[ -z "$SCRIPT_FILE" ] && SCRIPT_FILE=${BASH_SOURCE[0]} +# Save the starting errexit shell option for later restoration. +# Note that we do not choose the restoration method of running eval on +# the saved output of `set +o` as that would be problematic because: +# 1. bash turns off the errexit option in command substitutions +# and also does not clear errexit in a command substitution +# if `shopt -s inherit_errexit` is in effect: +# https://unix.stackexchange.com/a/383581. +# 2. oksh fails to restore the errexit option using this method. +# 3. oksh turns off the interactive option in command substitutions, +# and trying to toggle this option results in an error. +# 4. it clutters up the command history with lots of set commands. +# It is also more trouble than it is worth to try to work around +# these limitations just to restore one shell option we may have changed, +# not counting the posix and pipefail options as we want to keep them enabled. +case $- in + *e*) _STARTING_SH_OPTION_ERREXIT=1 ;; + *) _STARTING_SH_OPTION_ERREXIT=0 ;; +esac && { + # Unset function of the same name so that it is not invoked in place of + # the `command` regular built-in utility that we are going to be using: + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_09_01_01. + # In some ksh88 derivatives like the pdksh and XPG4 sh, the exit status + # will not be 0 if the function to unset is not already defined. + # In bash, there is no guarantee that the operation will succeed as: + # 1. readonly functions can not be unset. + # 2. all built-in utilities, including special built-in utilities + # like unset and exit, can be overridden by functions. + # 3. to make matters worse, even the reserved words can be overridden + # by aliases when not in POSIX mode. + # So we have no choice but to trust that the job is done faithfully. + # If secure shell scripting is ever a possibility, it is not found in bash. + \unset -f command && \: Suppress errexit if enabled. + \: Set a zero exit status for the grouping command. +} && + # https://pubs.opengroup.org/onlinepubs/9799919799/utilities/command.html + # The `command` built-in, when used without options, serves two purposes: + # 1. it causes the shell to treat the arguments as a simple command + # that is not subject to alias substitution and shell function lookup. + # 2. it suppresses the special characteristics of the special built-ins, + # so that an error does not cause a non-interactive script to abort. + # Though some shells do not respect point 2 above, + # so a prior test run in a subshell is still required. + # alias/unalias are not implemented in posh, hush and gash, and executing + # either in gash will exit the shell, so a safeguard is needed. + if (\command alias >/dev/null 2>&1) && + (\command unalias -a 2>/dev/null); then + # Save the starting aliases for later restoration, then unset + # all aliases asap as alias substitution occurs right before parsing: + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_03_01. + { + # The `-p` option used by several implementations is absolutely + # required in order to properly save and restore alias definitions. + (\command alias -p >/dev/null 2>&1) && + _STARTING_ALIASES=$(\command alias -p) || + _STARTING_ALIASES= + } && + \command unalias -a + else + _STARTING_ALIASES= + fi && { + # Enable POSIX mode. Needed for yash, as otherwise parsing will fail on + # non-ASCII characters if not supported by LC_CTYPE. + (command set -o posix 2>/dev/null) && command set -o posix + : Set a zero exit status for the grouping command. +} && + # Detect spoofing by external, readonly functions. + set -o errexit +# In case of a variable assignment error (or any other shell error): +# "In all of the cases shown in the table where an interactive shell +# is required not to exit, the shell shall not perform any further processing +# of the command in which the error occurred." +# ―https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_01 +# We are being extra cautious here by checking the exit status +# in a separate command because the words "any further processing of +# the command in which the error occurred" are too vague to be relied on. +# For example, if the following shell script is dot sourced in bash: +# ```sh +# readonly r +# r= || echo continue in OR list +# echo next command +# ``` +# only "next command" is shown in the output, whereas in most other shells +# no output is produced as the processing is halted at the dot source command. +# The behavior gets more complicated when we start using functions; +# add in the plethora of shell features and all consistency is lost. +case $? in + 0) \: ;; + *) + # "The behavior of return when not in a function or dot script + # differs between the System V shell and the KornShell. + # In the System V shell this is an error, + # whereas in the KornShell, the effect is the same as exit." + # ―https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_24_18 + \command return 69 2>/dev/null # Service unavailable. + \exit 69 # Service unavailable. + ;; +esac +download_file() { # args: url + # The try-finally construct can be implemented as a set of trap commands. + # However, it is notoriously difficult to write them portably and reliably. + # Since mktemp_ creates temporary files that are periodically cleared + # on any sane system, we leave it to the OS or the user + # to do the cleaning themselves for simplicity's sake. + temp=$(mktemp_) && + wget_ "$temp" "$1" >/dev/null 2>&1 && + printf '%s\n' "$temp" || { + print_error "Failed to download file from the URL: $1." + return "${_EX_UNAVAILABLE:?}" + } +} + +# An improvement on the "secure shell script" example demonstrated in +# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html#tag_20_22_17. +init() { + # Unset all functions whose name is found in the intersection of + # the standard utilities defined in POSIX.1-2017 and in POSIX.1-2024. + \set -- admin alias ar asa at awk basename batch bc bg cal cat cd cflow \ + chgrp chmod chown cksum cmp comm command compress cp crontab csplit \ + ctags cut cxref date dd delta df diff dirname du echo ed env ex \ + expand expr false fc fg file find fold fuser gencat get getconf \ + getopts grep hash head iconv id ipcrm ipcs jobs join kill lex link ln \ + locale localedef logger logname lp ls m4 mailx make man mesg mkdir \ + mkfifo more mv newgrp nice nl nm nohup od paste patch pathchk pax pr \ + printf prs ps pwd read renice rm rmdel rmdir sact sccs sed sh sleep \ + sort split strings strip stty tabs tail talk tee test time touch tput \ + tr true tsort tty type ulimit umask unalias uname uncompress unexpand \ + unget uniq unlink uucp uudecode uuencode uustat uux val vi wait wc \ + what who write xargs yacc zcat && { + # If the unset is unsuccessful, there are two known possibilities: + # 1. there are readonly functions (bash). + # 2. at least some in the list are not functions (ksh88). + # We check each in turn using the built-in utility `typeset` that is + # available in these shells to see if there are any defined function. + \unset -f "$@" || + while [ "$#" -gt 0 ]; do + # Evaluates to true if `typeset` is not available. + ! \command -- typeset -f "$1" >/dev/null && + \shift || { + # Reset $# to 0 to break the loop as $1 is a function. + \set -- + # Note that this command is not affected by errexit. + ! \: Set a non-zero exit status for the while loop. + } + done + } && + if (\unalias -a 2>/dev/null); then + # It is already too late to run the unalias command at this stage, + # but might still be useful in the case the script is dot sourced, + # acting as a reset mechanism. + \unalias -a + fi && + LC_ALL=C && + # To prevent the accidental insertion of SGR commands in grep's output, + # even when not directed at a terminal, we explicitly set + # the following three environment variables: + GREP_COLORS='mt=:ms=:mc=:sl=:cx=:fn=:ln=:bn=:se=' && + GREP_COLOR='0' && + GREP_OPTIONS= && + export LC_ALL GREP_COLORS GREP_COLOR GREP_OPTIONS && { + path=$(command -p getconf PATH 2>/dev/null) && + PATH="$path:$PATH" && + export PATH || + test "$?" -eq 127 # getconf: command not found (Haiku). + } && { + # The pipefail option was added in POSIX.1-2024 (SUSv5), + # and has long been supported by most major POSIX-compatible shells, + # with the notable exceptions of dash and ksh88-based shells. + # There are some caveats to switching on this option though: + # https://mywiki.wooledge.org/BashPitfalls#set_-euo_pipefail. + (command set -o pipefail 2>/dev/null) && + command set -o pipefail || + : Do without. + } && { + # In XPG4 sh, `unset -f '['` is an error. + # In bash 3, `command` always exits the shell on failure + # when errexit is on, even if guarded by AND/OR lists. + (unset -f '[') 2>/dev/null && + unset -f '[' 2>/dev/null || + command -V '[' | { ! grep -q function; } + } && + IFS=$(printf '%b' ' \n\t') && + umask 0077 && # cp/mv need execute access to parent directories. + # Inspired by https://stackoverflow.com/q/1101957. + exit_status_definitions() { + cut -d'#' -f1 <<'EOF' +_EX_OK=0 # Successful exit status. +_EX_FAIL=1 # Failed exit status. +_EX_USAGE=2 # Command line usage error. +_EX__BASE=64 # Base value for error messages. +_EX_DATAERR=65 # Data format error. +_EX_NOINPUT=66 # Cannot open input. +_EX_NOUSER=67 # Addressee unknown. +_EX_NOHOST=68 # Host name unknown. +_EX_UNAVAILABLE=69 # Service unavailable. +_EX_SOFTWARE=70 # Internal software error. +_EX_OSERR=71 # System error (e.g., can't fork). +_EX_OSFILE=72 # Critical OS file missing. +_EX_CANTCREAT=73 # Can't create (user) output file. +_EX_IOERR=74 # Input/output error. +_EX_TEMPFAIL=75 # Temp failure; user is invited to retry. +_EX_PROTOCOL=76 # Remote error in protocol. +_EX_NOPERM=77 # Permission denied. +_EX_CONFIG=78 # Configuration error. +_EX_NOEXEC=126 # A file to be executed was found, but it was not an executable utility. +_EX_CNF=127 # A utility to be executed was not found. +EOF + } && + exit_status_definitions >/dev/null || { + echo 'Failed to initialize the environment.' >&2 + return 69 # Service unavailable. + } + name= && status_= || return + while IFS='= ' read -r name status_; do + # "When reporting the exit status with the special parameter '?', + # the shell shall report the full eight bits of exit status available." + # ―https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02 + # "The exit status shall be n, if specified, except that + # the behavior is unspecified if n is not an unsigned decimal integer + # or is greater than 255." + # ―https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_21_14 + is_integer "$status_" && + [ "$status_" -ge 0 ] && [ "$status_" -le 255 ] || { + printf '%s %s\n' 'Undefined exit status in the definition:' \ + "$name=$status_." >&2 + return 70 # Internal software error. + } + (eval "$name=" 2>/dev/null) && + eval "$name=$status_" && + eval readonly "$name" || { + eval [ "\"\$$name\"" = "$status_" ] && + continue # $name is already readonly and set to $status_. + printf '%s %s\n' \ + "Failed to assign $status_ to $name and make $name readonly." \ + 'Try again in a new shell environment?' >&2 + return 75 # Temp failure. + } + done <&2 +} + +print_info() { # args: [string ...] + printf '%b' "$*" >&2 +} + +print_missing() { # args: [string ...] + print_error "Failed to find the following utilities on your system: $*." + return "${_EX_CNF:?}" +} + +print_ok() { # args: [string ...] + printf '%s\n' "${_TPUT_AF_GREEN?}OK: $*${_TPUT_SGR0?}" >&2 +} + +print_warning() { # args: [string ...] + printf '%s\n' "${_TPUT_AF_YELLOW?}WARNING: $*${_TPUT_SGR0?}" >&2 +} + +print_yN() { # args: [string ...] + printf '%s' "${_TPUT_AF_RED?}$* [y/N]${_TPUT_SGR0?} " >&2 +} + +probe_fuser_() { + fuser_() { # args: file + output= || return + case ${FUSER__IMPLEMENTATION?} in + fuser) + # Do not add --, as on Ubuntu and Arch Linux at least, + # fuser does not conform to the XBD Utility Syntax Guidelines. + output=$(command fuser "$1" 2>/dev/null) + ;; + lsof) + # BusyBox lsof ignores all options and arguments. + output=$(command lsof -lnPt -- "$1") + ;; + fstat) + # Begin after the header line. + # Not functional on DragonFly 6.4 if used with an argument. + output=$(command fstat -- "$1" | tail -n +2) + ;; + fdinfo) # Haiku + # Do not add --, as fdinfo does not conform to the + # XBD Utility Syntax Guidelines. + output=$(command fdinfo -f "$1") + ;; + *) + print_missing fuser lsof fstat fdinfo + return + ;; + esac && [ -n "$output" ] + } || return + util= && set -- fuser lsof fstat fdinfo || return + for util in "$@"; do + if command -v -- "$util" >/dev/null; then + [ "${FUSER__IMPLEMENTATION:-"$util"}" != "$util" ] || { + FUSER__IMPLEMENTATION=$util + return + } + fi + done + print_missing "${FUSER__IMPLEMENTATION:-"$@"}" +} + +probe_mktemp_() { + mktemp_() { + case ${MKTEMP__IMPLEMENTATION?} in + mktemp) command mktemp ;; + m4) + # Copied from https://unix.stackexchange.com/a/181996. + echo 'mkstemp(template)' | + m4 -D template="${TMPDIR:-/tmp}/baseXXXXXX" + ;; + *) print_missing mktemp m4 ;; + esac + } || return + util= && set -- mktemp m4 || return + for util in "$@"; do + if command -v -- "$util" >/dev/null; then + [ "${MKTEMP__IMPLEMENTATION:-"$util"}" != "$util" ] || { + MKTEMP__IMPLEMENTATION=$util + return + } + fi + done + print_missing "${MKTEMP__IMPLEMENTATION:-"$@"}" +} + +probe_realpath_() { + # Adjusted from https://github.com/ko1nksm/readlinkf. + # Limitation: `readlinkf` cannot handle filenames that end with a newline. + # Execute in a subshell to localize variables and the effect of cd. + readlinkf() ( # args: file + [ "${1:-}" ] || return "${_EX_FAIL:?}" + # The maximum depth of symbolic links is 40. + # This value is the same as defined in the Linux 5.6 kernel. + # However, `readlink -f` has no such limitation. + max_symlinks=40 + CDPATH= # To avoid changing to an unexpected directory. + target=$1 + [ -e "${target%/}" ] || + target=${1%"${1##*[!/]}"} # Trim trailing slashes. + [ -d "${target:-/}" ] && target="$target/" + cd -P . 2>/dev/null || return "${_EX_FAIL:?}" + while [ "$max_symlinks" -ge 0 ] && + max_symlinks=$((max_symlinks - 1)); do + if [ ! "$target" = "${target%/*}" ]; then + case $target in + /*) cd -P "${target%/*}/" 2>/dev/null || break ;; + *) cd -P "./${target%/*}" 2>/dev/null || break ;; + esac + target=${target##*/} + fi + if [ ! -L "$target" ]; then + target="${PWD%/}${target:+/}${target}" + printf '%s\n' "${target:-/}" + return "${_EX_OK:?}" + fi + # `ls -dl` format: "%s %u %s %s %u %s %s -> %s\n", + # , , , , + # , , , + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ls.html + link=$(ls -dl -- "$target" 2>/dev/null) || break + target=${link#*" $target -> "} + done + return "${_EX_FAIL:?}" + ) || return + realpath_() { # args: file ... + if [ "$#" -eq 0 ]; then + echo 'realpath_: missing operand' >&2 + return "${_EX_USAGE:?}" + else + return_status=${_EX_OK:?} || return + while [ "$#" -gt 0 ]; do + case ${REALPATH__IMPLEMENTATION?} in + realpath) command realpath -- "$1" ;; + readlink) command readlink -f -- "$1" ;; + grealpath) command grealpath -- "$1" ;; + greadlink) command greadlink -f -- "$1" ;; + *) readlinkf "$1" ;; + esac + status_=$? || return + [ "$status_" -eq "${_EX_OK:?}" ] || return_status=$status_ + shift + done + return "$return_status" + fi + } || return + # Both realpath and readlink -f as found on the BSDs are quite different + # from their Linux counterparts and even among themselves, + # instead behaving similarly to the POSIX realpath -e for the most part. + # The table below details the varying behaviors where the non-header cells + # note the exit status followed by any output in parentheses: + # | | realpath nosuchfile | realpath nosuchtarget | readlink -f nosuchfile | readlink -f nosuchtarget | + # |---------------|---------------------|-----------------------|------------------------|--------------------------| + # | FreeBSD 14.2 | 1 (error message) | 1 (error message) | 1 | 1 (fully resolved path) | + # | OpenBSD 7.6 | 1 (error message) | 1 (error message) | 1 (error message) | 1 (error message) | + # | NetBSD 10.0 | 0 | 0 | 1 | 1 (fully resolved path) | + # | DragonFly 6.4 | 1 (error message) | 1 (error message) | 1 | 1 (input path argument) | + # It is also worth pointing out that the BusyBox (v1.37.0) + # realpath and readlink -f exit with status 1 without outputting + # the fully resolved path if the argument contains no slash characters + # and does not name a file in the current directory. + name=$(uname) && + case $name in + NetBSD) : ;; # NetBSD realpath works as intended. + Darwin | *BSD | DragonFly) # Other BSDs and macOS should use readlinkf. + REALPATH__IMPLEMENTATION=${REALPATH__IMPLEMENTATION:-readlinkf} + ;; + esac || + return + util= && set -- realpath readlink greadlink || return + for util in "$@"; do + if case $util in + readlink | greadlink) command "$util" -f -- . >/dev/null 2>&1 ;; + *) command "$util" -- . >/dev/null 2>&1 ;; + esac then + [ "${REALPATH__IMPLEMENTATION:-"$util"}" != "$util" ] || { + REALPATH__IMPLEMENTATION=$util + return + } + fi + done + REALPATH__IMPLEMENTATION=readlinkf +} + +# https://mywiki.wooledge.org/BashFAQ/037 +probe_terminal() { + # Testing for multiple terminal capabilities at once is unreliable, + # and the non-POSIX option -S is not recognized by NetBSD's tput, + # which also requires a numerical argument after setaf/AF, + # so we test thus, trying both terminfo and termcap names just in case: + if [ -t 2 ]; then + tput setaf 0 >/dev/null 2>&1 && + tput bold >/dev/null 2>&1 && + tput sgr0 >/dev/null 2>&1 && + _TPUT_AF_RED=$(tput setaf 1) && + _TPUT_AF_GREEN=$(tput setaf 2) && + _TPUT_AF_YELLOW=$(tput setaf 3) && + _TPUT_AF_BLUE=$(tput setaf 4) && + _TPUT_AF_CYAN=$(tput setaf 6) && + _TPUT_BOLD_AF_BLUE=$(tput bold)$(tput setaf 4) && + _TPUT_SGR0=$(tput sgr0) && + return + tput AF 0 >/dev/null 2>&1 && + tput md >/dev/null 2>&1 && + tput me >/dev/null 2>&1 && + _TPUT_AF_RED=$(tput AF 1) && + _TPUT_AF_GREEN=$(tput AF 2) && + _TPUT_AF_YELLOW=$(tput AF 3) && + _TPUT_AF_BLUE=$(tput AF 4) && + _TPUT_AF_CYAN=$(tput AF 6) && + _TPUT_BOLD_AF_BLUE=$(tput md)$(tput AF 4) && + _TPUT_SGR0=$(tput me) && + return + fi + _TPUT_AF_RED= && + _TPUT_AF_GREEN= && + _TPUT_AF_YELLOW= && + _TPUT_AF_BLUE= && + _TPUT_AF_CYAN= && + _TPUT_BOLD_AF_BLUE= && + _TPUT_SGR0= +} + +probe_wget_() { + wget_() { # args: file url + case ${WGET__IMPLEMENTATION?} in + curl) + status_=$( + command curl -sSfw '%{http_code}' -o "$1" -- "$2" + ) && + is_integer "$status_" && + [ "$status_" -ge 200 ] && [ "$status_" -lt 300 ] + ;; + wget) command wget -O "$1" -- "$2" ;; + fetch) command fetch -o "$1" -- "$2" ;; + ftp) command ftp -o "$1" -- "$2" ;; # Progress meter to stdout. + *) print_missing curl wget fetch ftp ;; + esac + } || return + util= && set -- curl wget fetch ftp || return + for util in "$@"; do + if command -v -- "$util" >/dev/null; then + [ "${WGET__IMPLEMENTATION:-"$util"}" != "$util" ] || { + WGET__IMPLEMENTATION=$util + return + } + fi + done + print_missing "${WGET__IMPLEMENTATION:-"$@"}" +} + +# Copied from https://unix.stackexchange.com/a/464963. +read1() { # args: name + if [ -t 0 ]; then + # If stdin is a tty device, put it out of icanon, + # set min and time to sane values, but do not otherwise + # touch other inputs or local settings (echo, isig, icrnl...). + # Take a backup of the previous settings beforehand. + saved_tty_settings=$(stty -g) + stty -icanon min 1 time 0 + fi + eval "$1=" + while + # Read one byte, using a workaround for the fact that + # command substitution strips trailing newline characters. + c=$( + dd bs=1 count=1 2>/dev/null + echo . + ) + c=${c%.} + # Break out of the loop on empty input (eof) + # or if a full character has been accumulated in the output variable + # (using `wc -m` to count the number of characters). + [ -n "$c" ] && + eval "$1=\${$1}"'$c + [ "$(($(printf %s "${'"$1"'}" | wc -m)))" -eq 0 ]' + do + continue + done + if [ -t 0 ]; then + # Restore settings saved earlier if stdin is a tty device. + stty "$saved_tty_settings" + fi +} + +# https://kb.mozillazine.org/Profile_folder_-_Firefox#Files +# https://searchfox.org/mozilla-central/source/toolkit/profile/nsProfileLock.cpp +arkenfox_check_firefox_profile_lock() { # args: directory + flock_file=${1%/}/.parentlock || return + [ -e "$flock_file" ] || [ -L "$flock_file" ] || { + print_error "Failed to find the .parentlock file under $1." + return "${_EX_NOINPUT:?}" + } + # This way of writing the while loop ensures the proper exit status + # is returned to the caller function. + while :; do + fuser_ "$flock_file" || + arkenfox_is_firefox_profile_symlink_locked "$1" || { + # An exit status of _EX__BASE indicates that + # the user wishes to proceed with the program. + [ "$?" -eq "${_EX__BASE:?}" ] || return + break + } + print_warning 'This Firefox profile seems to be in use.' \ + 'Close Firefox and try again.' + print_info '\nPress any key to continue. ' + read1 REPLY + print_info '\n\n' + done +} + +arkenfox_check_nonroot() { + name=$(uname) || return + # Haiku is a single-user operating system. + [ "$name" != 'Haiku' ] || return "${_EX_OK:?}" + id=$(id -u) || return + if is_integer "$id" && [ "$id" -eq 0 ]; then + print_error "You shouldn't run this with elevated privileges" \ + '(such as with doas/sudo).' + return "${_EX_USAGE:?}" + fi +} + +arkenfox_is_firefox_profile_symlink_locked() { # args: directory + name=$(uname) && + if [ "$name" = 'Darwin' ]; then # macOS + lock_file=${1%/}/.parentlock + else + lock_file=${1%/}/lock + fi || + return + [ -L "$lock_file" ] || { + print_error "$lock_file is not a symbolic link!" + return "${_EX_DATAERR:?}" + } + target=$(realpath_ "$lock_file") && + lock_signature=$( + basename -- "$target" | + # Character classes and range expressions are locale-dependent: + # https://unix.stackexchange.com/a/654391. + sed -n 's/^\(.*\):+\{0,1\}\([0123456789]\{1,\}\)$/\1:\2/p' + ) && + [ -n "$lock_signature" ] && + lock_acquired_ip=${lock_signature%:*} && + lock_acquired_pid=${lock_signature##*:} || { + print_error 'Failed to resolve the symlink target signature' \ + "of the lock file: $lock_file." + return "${_EX_DATAERR:?}" + } + if [ "$lock_acquired_ip" = '127.0.0.1' ]; then + kill -s 0 "$lock_acquired_pid" 2>/dev/null + [ "$?" -eq "${_EX_OK:?}" ] && return || return "${_EX__BASE:?}" + else + print_warning 'Unable to determine if the Firefox profile' \ + 'is being used or not.' + print_yN 'Proceed anyway?' + read1 REPLY || return + print_info '\n\n' + [ "$REPLY" = 'Y' ] || [ "$REPLY" = 'y' ] || return "${_EX_OK:?}" + return "${_EX__BASE:?}" + fi +} + +arkenfox_script_version() { # args: file + # Character classes and range expressions are locale-dependent: + # https://unix.stackexchange.com/a/654391. + format='[0123456789]\{1,\}\.[0123456789]\{1,\}' && + version=$( + sed -n -- "5s/.*version:[[:blank:]]*\($format\).*/\1/p" "$1" + ) && + [ -n "$version" ] && + printf '%s\n' "$version" || { + print_error "Failed to determine the version of the script file: $1." + return "${_EX_DATAERR:?}" + } +} -AUTOUPDATE=true -QUICKSTART=false +arkenfox_select_firefox_profile() { # args: file + while :; do + # Adapted from https://unix.stackexchange.com/a/786827. + profiles=$( + # Character classes and range expressions are locale-dependent: + # https://unix.stackexchange.com/a/654391. + awk -- '/^[[]/ { section = substr($0, 1) } + (section ~ /^[[]Profile[0123456789]+[]]$/) { print }' "$1" + ) && + profile_count=$( + printf '%s' "$profiles" | + grep -Ec '^[[]Profile[0123456789]+[]]$' + ) && + is_integer "$profile_count" && [ "$profile_count" -gt 0 ] || { + print_error 'Failed to find the profile sections in the INI file.' + return "${_EX_DATAERR:?}" + } + if [ "$profile_count" -eq 1 ]; then + printf '%s\n' "$profiles" + return + else + profiles_display=$( + printf '%s\n\n' "$profiles" | + grep -Ev -e '^IsRelative=' -e '^Default=' && + awk -- '/^[[]/ { section = substr($0, 2) } + ((section ~ /^Install/) && /^Default=/) \ + { print }' "$1" + ) || return + cat >&2 </dev/null # Service unavailable. + \exit 69 # Service unavailable. + ;; +esac + +arkenfox_prefs_cleaner_init() { + probe_terminal && probe_realpath_ || return + # IMPORTANT: ARKENFOX_PREFS_CLEANER_NAME must be synced to + # the name of this file! + # This is so that we may somewhat determine if the script is sourced or not + # by comparing it to the basename of the canonical path of $0, + # which should be better than hard coding all the names of + # the interactive and non-interactive POSIX shells in existence. + # Cf. https://stackoverflow.com/a/28776166. + ARKENFOX_PREFS_CLEANER_NAME=${ARKENFOX_PREFS_CLEANER_NAME:-prefsCleaner.sh} || return + (_ARKENFOX_REPO_DOWNLOAD_URL_ROOT=) 2>/dev/null && + _ARKENFOX_REPO_DOWNLOAD_URL_ROOT='https://raw.githubusercontent.com/arkenfox/user.js/master' && + readonly _ARKENFOX_REPO_DOWNLOAD_URL_ROOT || + test "$_ARKENFOX_REPO_DOWNLOAD_URL_ROOT" = \ + 'https://raw.githubusercontent.com/arkenfox/user.js/master' || { + print_error 'Failed to assign the arkenfox repository download URL' \ + 'and make it readonly. Try again in a new shell environment?' + return "${_EX_TEMPFAIL:?}" + } + path=$(realpath_ "$0") && + dir_name=$(dirname -- "$path") && + base_name=$(basename -- "$path") || { + print_error 'Failed to resolve the run file path.' + return "${_EX_UNAVAILABLE:?}" + } + ( + _ARKENFOX_PREFS_CLEANER_RUN_PATH= && + _ARKENFOX_PREFS_CLEANER_RUN_DIR= && + _ARKENFOX_PREFS_CLEANER_RUN_NAME= + ) 2>/dev/null && + _ARKENFOX_PREFS_CLEANER_RUN_PATH=$path && + _ARKENFOX_PREFS_CLEANER_RUN_DIR=$dir_name && + _ARKENFOX_PREFS_CLEANER_RUN_NAME=$base_name && + readonly _ARKENFOX_PREFS_CLEANER_RUN_PATH \ + _ARKENFOX_PREFS_CLEANER_RUN_DIR \ + _ARKENFOX_PREFS_CLEANER_RUN_NAME || { + [ "$_ARKENFOX_PREFS_CLEANER_RUN_PATH" = "$path" ] && + [ "$_ARKENFOX_PREFS_CLEANER_RUN_DIR" = "$dir_name" ] && + [ "$_ARKENFOX_PREFS_CLEANER_RUN_NAME" = "$base_name" ] || { + print_error 'Failed to make the resolved run file path readonly.' \ + 'Try again in a new shell environment?' + return "${_EX_TEMPFAIL:?}" + } + } +} + +arkenfox_prefs_cleaner() { # args: [option ...] + arkenfox_prefs_cleaner_parse_options "$@" || return + arkenfox_prefs_cleaner_exec_general_options || { + status_=$? || return + # An exit status of _EX__BASE indicates that a command tied to + # a general option has been executed successfully. + [ "$status_" -eq "${_EX__BASE:?}" ] && + return "${_EX_OK:?}" || + return "$status_" + } + arkenfox_check_nonroot && + arkenfox_prefs_cleaner_update_self "$@" && + if is_option_set \ + "${_ARKENFOX_PREFS_CLEANER_OPTION_S_START_IMMEDIATELY?}"; then + arkenfox_prefs_cleaner_start + else + print_info 'In order to proceed, select a command below' \ + 'by entering its corresponding number.\n\n' + while print_info '1) Start\n2) Help\n3) Exit\n'; do + while print_info '#? ' && read -r REPLY; do + case $REPLY in + 1) + arkenfox_prefs_cleaner_start + return + ;; + 2) + arkenfox_prefs_cleaner_usage + arkenfox_prefs_cleaner_help + return + ;; + 3) return ;; + '') break ;; + *) : ;; + esac + done + done + fi +} -## download method priority: curl -> wget -DOWNLOAD_METHOD='' -if command -v curl >/dev/null; then - DOWNLOAD_METHOD='curl --max-redirs 3 -so' -elif command -v wget >/dev/null; then - DOWNLOAD_METHOD='wget --max-redirect 3 --quiet -O' -else - AUTOUPDATE=false - echo -e "No curl or wget detected.\nAutomatic self-update disabled!" -fi +arkenfox_prefs_cleaner_usage() { + cat >&2 <&2 - exit $1 +Usage: ${ARKENFOX_PREFS_CLEANER_NAME:?} [-hdsl] [-p PROFILE] + +Options: + -h Show this help message and exit. + -d Don't auto-update prefsCleaner.sh. + -s Start immediately. + -p PROFILE Path to your Firefox profile (if different from the containing directory of this script). + IMPORTANT: If the path contains spaces, wrap the entire argument in quotes. + -l Choose your Firefox profile from a list. + +EOF +} + +arkenfox_prefs_cleaner_parse_options() { # args: [option ...] + name= && + # OPTIND must be manually reset between multiple calls to getopts. + OPTIND=1 && + _ARKENFOX_PREFS_CLEANER_OPTIONS_DISJOINT=0 && + _ARKENFOX_PREFS_CLEANER_OPTION_H_HELP= && + _ARKENFOX_PREFS_CLEANER_OPTION_D_DONT_UPDATE= && + _ARKENFOX_PREFS_CLEANER_OPTION_S_START_IMMEDIATELY= && + _ARKENFOX_PREFS_CLEANER_OPTION_P_PROFILE_PATH= && + _ARKENFOX_PREFS_CLEANER_OPTION_L_LIST_PROFILES= || + return + while getopts 'hdsp:l' name; do + ! is_option_set "$_ARKENFOX_PREFS_CLEANER_OPTIONS_DISJOINT" || { + arkenfox_prefs_cleaner_usage + return "${_EX_USAGE:?}" + } + case $name in + h) + _ARKENFOX_PREFS_CLEANER_OPTION_H_HELP=1 + _ARKENFOX_PREFS_CLEANER_OPTIONS_DISJOINT=1 + ;; + d) _ARKENFOX_PREFS_CLEANER_OPTION_D_DONT_UPDATE=1 ;; + s) _ARKENFOX_PREFS_CLEANER_OPTION_S_START_IMMEDIATELY=1 ;; + p) _ARKENFOX_PREFS_CLEANER_OPTION_P_PROFILE_PATH=$OPTARG ;; + l) _ARKENFOX_PREFS_CLEANER_OPTION_L_LIST_PROFILES=1 ;; + \?) + arkenfox_prefs_cleaner_usage + return "${_EX_USAGE:?}" + ;; + :) return "${_EX_USAGE:?}" ;; + esac + done +} + +arkenfox_prefs_cleaner_exec_general_options() { + if is_option_set "${_ARKENFOX_PREFS_CLEANER_OPTION_H_HELP?}"; then + arkenfox_prefs_cleaner_usage 2>&1 + else + return "${_EX_OK:?}" + fi + # We want to return from the caller function as well + # if a command tied to a general option is executed. + # To achieve that, we translate an exit status of _EX_OK to _EX__BASE + # and handle the retranslation back to its original exit status + # in the caller function. + status_=$? || return + if [ "$status_" -eq "${_EX_OK:?}" ]; then + return "${_EX__BASE:?}" + else + return "$status_" + fi } - -fUsage() { - echo -e "\nUsage: $0 [-ds]" - echo -e " -Optional Arguments: - -s Start immediately - -d Don't auto-update prefsCleaner.sh" + +arkenfox_prefs_cleaner_update_self() { # args: [option ...] + is_option_set "${_ARKENFOX_PREFS_CLEANER_OPTION_D_DONT_UPDATE?}" && + return "${_EX_OK:?}" || { + probe_mktemp_ && + probe_wget_ || { + print_warning 'No download feature is absent.' \ + 'Automatic self-update disabled!' + return "${_EX_OK:?}" + } + } + downloaded_file=$( + download_file "${_ARKENFOX_REPO_DOWNLOAD_URL_ROOT:?}/prefsCleaner.sh" + ) || + return + local_version=$( + arkenfox_script_version "${_ARKENFOX_PREFS_CLEANER_RUN_PATH:?}" + ) && + downloaded_version=$( + arkenfox_script_version "$downloaded_file" + ) && + local_version_major=${local_version%%.*} && + is_integer "$local_version_major" && + local_version_minor=${local_version#*.} && + is_integer "$local_version_minor" && + downloaded_version_major=${downloaded_version%%.*} && + is_integer "$downloaded_version_major" && + downloaded_version_minor=${downloaded_version#*.} && + is_integer "$downloaded_version_minor" || { + print_error 'Failed to obtain valid version parts for comparison.' + return "${_EX_DATAERR:?}" + } + if [ "$local_version_major" -eq "$downloaded_version_major" ] && + [ "$local_version_minor" -lt "$downloaded_version_minor" ] || + [ "$local_version_major" -lt "$downloaded_version_major" ]; then + # Suppress diagnostic message on FreeBSD/DragonFly + # (mv: set owner/group: Operation not permitted). + mv -f -- \ + "$downloaded_file" \ + "${_ARKENFOX_PREFS_CLEANER_RUN_PATH:?}" 2>/dev/null && + chmod -- u+rx "${_ARKENFOX_PREFS_CLEANER_RUN_PATH:?}" || { + print_error 'Failed to update the arkenfox prefs.js cleaner' \ + 'and make it executable.' + return "${_EX_CANTCREAT:?}" + } + "${_ARKENFOX_PREFS_CLEANER_RUN_PATH:?}" -d "$@" + fi } - -download_file() { # expects URL as argument ($1) - declare -r tf=$(mktemp) - - $DOWNLOAD_METHOD "${tf}" "$1" &>/dev/null && echo "$tf" || echo '' # return the temp-filename or empty string on error -} - -fFF_check() { - # there are many ways to see if firefox is running or not, some more reliable than others - # this isn't elegant and might not be future-proof but should at least be compatible with any environment - while [ -e lock ]; do - echo -e "\nThis Firefox profile seems to be in use. Close Firefox and try again.\n" >&2 - read -r -p "Press any key to continue." - done -} - -## returns the version number of a prefsCleaner.sh file -get_prefsCleaner_version() { - echo "$(sed -n '5 s/.*[[:blank:]]\([[:digit:]]*\.[[:digit:]]*\)/\1/p' "$1")" -} - -## updates the prefsCleaner.sh file based on the latest public version -update_prefsCleaner() { - declare -r tmpfile="$(download_file 'https://raw.githubusercontent.com/arkenfox/user.js/master/prefsCleaner.sh')" - [ -z "$tmpfile" ] && echo -e "Error! Could not download prefsCleaner.sh" && return 1 # check if download failed - - [[ $(get_prefsCleaner_version "$SCRIPT_FILE") == $(get_prefsCleaner_version "$tmpfile") ]] && return 0 - - mv "$tmpfile" "$SCRIPT_FILE" - chmod u+x "$SCRIPT_FILE" - "$SCRIPT_FILE" "$@" -d - exit 0 -} - -fClean() { - # the magic happens here - prefs="@@" - prefexp="user_pref[ ]*\([ ]*[\"']([^\"']+)[\"'][ ]*," - while read -r line; do - if [[ "$line" =~ $prefexp && $prefs != *"@@${BASH_REMATCH[1]}@@"* ]]; then - prefs="${prefs}${BASH_REMATCH[1]}@@" - fi - done <<< "$(grep -E "$prefexp" user.js)" - - while IFS='' read -r line || [[ -n "$line" ]]; do - if [[ "$line" =~ ^$prefexp ]]; then - if [[ $prefs != *"@@${BASH_REMATCH[1]}@@"* ]]; then - echo "$line" - fi - else - echo "$line" - fi - done < "$1" > prefs.js -} - -fStart() { - if [ ! -e user.js ]; then - fQuit 1 "user.js not found in the current directory." - elif [ ! -e prefs.js ]; then - fQuit 1 "prefs.js not found in the current directory." - fi - - fFF_check - mkdir -p prefsjs_backups - bakfile="prefsjs_backups/prefs.js.backup.$(date +"%Y-%m-%d_%H%M")" - mv prefs.js "${bakfile}" || fQuit 1 "Operation aborted.\nReason: Could not create backup file $bakfile" - echo -e "\nprefs.js backed up: $bakfile" - echo "Cleaning prefs.js..." - fClean "$bakfile" - fQuit 0 "All done!" -} - - -while getopts "sd" opt; do - case $opt in - s) - QUICKSTART=true - ;; - d) - AUTOUPDATE=false - ;; - esac -done - -## change directory to the Firefox profile directory -cd "$(dirname "${SCRIPT_FILE}")" - -# Check if running as root and if any files have the owner as root/wheel. -if [ "${EUID:-"$(id -u)"}" -eq 0 ]; then - fQuit 1 "You shouldn't run this with elevated privileges (such as with doas/sudo)." -elif [ -n "$(find ./ -user 0)" ]; then - printf 'It looks like this script was previously run with elevated privileges, -you will need to change ownership of the following files to your user:\n' - find . -user 0 - fQuit 1 -fi - -[ "$AUTOUPDATE" = true ] && update_prefsCleaner "$@" - -echo -e "\n\n" -echo " ╔══════════════════════════╗" -echo " ║ prefs.js cleaner ║" -echo " ║ by claustromaniac ║" -echo " ║ v2.1 ║" -echo " ╚══════════════════════════╝" -echo -e "\nThis script should be run from your Firefox profile directory.\n" -echo "It will remove any entries from prefs.js that also exist in user.js." -echo "This will allow inactive preferences to be reset to their default values." -echo -e "\nThis Firefox profile shouldn't be in use during the process.\n" - -[ "$QUICKSTART" = true ] && fStart - -echo -e "\nIn order to proceed, select a command below by entering its corresponding number.\n" - -select option in Start Help Exit; do - case $option in - Start) - fStart - ;; - Help) - fUsage - echo -e "\nThis script creates a backup of your prefs.js file before doing anything." - echo -e "It should be safe, but you can follow these steps if something goes wrong:\n" - echo "1. Make sure Firefox is closed." - echo "2. Delete prefs.js in your profile folder." - echo "3. Delete Invalidprefs.js if you have one in the same folder." - echo "4. Rename or copy your latest backup to prefs.js." - echo "5. Run Firefox and see if you notice anything wrong with it." - echo "6. If you do notice something wrong, especially with your extensions, and/or with the UI, go to about:support, and restart Firefox with add-ons disabled. Then, restart it again normally, and see if the problems were solved." - echo -e "If you are able to identify the cause of your issues, please bring it up on the arkenfox user.js GitHub repository.\n" - ;; - Exit) - fQuit 0 - ;; - esac -done - -fQuit 0 + +arkenfox_prefs_cleaner_start() { + probe_mktemp_ && + probe_fuser_ && + arkenfox_prefs_cleaner_set_profile_path || + return + [ -f "${_ARKENFOX_PROFILE_USERJS:?}" ] && + [ -f "${_ARKENFOX_PROFILE_PREFSJS:?}" ] || { + print_error 'Failed to find both user.js and prefs.js' \ + "in the profile path: ${_ARKENFOX_PROFILE_PATH:?}." + return "${_EX_NOINPUT:?}" + } + arkenfox_prefs_cleaner_banner + arkenfox_check_firefox_profile_lock "${_ARKENFOX_PROFILE_PATH:?}" && + backup=${_ARKENFOX_PROFILE_PREFSJS_BACKUP_DIR:?} && + backup=${backup%/}/prefs.js.backup.$(date +"%Y-%m-%d_%H%M") || + return + # Add the -p option so that mkdir does not return a >0 exit status + # if any of the specified directories already exists. + mkdir -p -- "${_ARKENFOX_PROFILE_PREFSJS_BACKUP_DIR:?}" && + cp -f -- "${_ARKENFOX_PROFILE_PREFSJS:?}" "$backup" || { + print_error 'Failed to backup prefs.js:' \ + "${_ARKENFOX_PROFILE_PREFSJS:?}." + return "${_EX_CANTCREAT:?}" + } + print_ok "Your prefs.js has been backed up: $backup." + print_info 'Cleaning prefs.js...\n\n' + arkenfox_prefs_cleaner_clean "$backup" || return + print_ok 'All done!' +} + +arkenfox_prefs_cleaner_set_profile_path() { + if [ -n "${_ARKENFOX_PREFS_CLEANER_OPTION_P_PROFILE_PATH?}" ]; then + _ARKENFOX_PROFILE_PATH=${_ARKENFOX_PREFS_CLEANER_OPTION_P_PROFILE_PATH?} + elif is_option_set \ + "${_ARKENFOX_PREFS_CLEANER_OPTION_L_LIST_PROFILES?}"; then + _ARKENFOX_PROFILE_PATH=$(arkenfox_select_firefox_profile_path) + else + _ARKENFOX_PROFILE_PATH=${_ARKENFOX_PREFS_CLEANER_RUN_DIR:?} + fi && + _ARKENFOX_PROFILE_PATH=$(realpath_ "$_ARKENFOX_PROFILE_PATH") || + return + [ -w "$_ARKENFOX_PROFILE_PATH" ] && + cd -- "$_ARKENFOX_PROFILE_PATH" || { + print_error 'The path to your Firefox profile' \ + "('$_ARKENFOX_PROFILE_PATH') failed to be a directory to which" \ + 'the user has both write and execute access.' + return "${_EX_UNAVAILABLE:?}" + } + _ARKENFOX_PROFILE_USERJS=${_ARKENFOX_PROFILE_PATH%/}/user.js && + _ARKENFOX_PROFILE_PREFSJS=${_ARKENFOX_PROFILE_PATH%/}/prefs.js && + _ARKENFOX_PROFILE_PREFSJS_BACKUP_DIR=${_ARKENFOX_PROFILE_PATH%/}/prefsjs_backups +} + +arkenfox_prefs_cleaner_banner() { + cat >&2 <<'EOF' + + + + ╔══════════════════════════╗ + ║ prefs.js cleaner ║ + ║ by claustromaniac ║ + ║ v3.0 ║ + ╚══════════════════════════╝ + +This script will remove all entries from prefs.js that also exist in user.js. +This will allow inactive preferences to be reset to their default values. + +This Firefox profile shouldn't be in use during the process. + + +EOF +} + +arkenfox_prefs_cleaner_clean() { # args: file + format="user_pref[[:blank:]]*\([[:blank:]]*[\"']([^\"']+)[\"'][[:blank:]]*," && + # SunOS/OpenBSD's grep do not recognize - as stdin, + # so we create temp files for use as pattern files. + prefs_in_userjs=$(mktemp_) && + prefs_to_clean=$(mktemp_) && + grep -E -- "$format" "${_ARKENFOX_PROFILE_USERJS:?}" | + awk -F"[\"']" '{ print "\"" $2 "\""; print "'\''" $2 "'\''"; }' | + sort | + uniq >|"$prefs_in_userjs" || + return + grep -F -f "$prefs_in_userjs" -- "$1" | + grep -E -e "^[[:blank:]]*$format" >|"$prefs_to_clean" || + # It is not an error if there are no prefs to clean. + [ "$?" -eq "${_EX_FAIL:?}" ] || + return + if [ -s "$prefs_to_clean" ]; then # File size is greater than zero. + temp=$(mktemp_) && + grep -F -v -f "$prefs_to_clean" -- "$1" >|"$temp" && + # Suppress diagnostic message on FreeBSD/DragonFly + # (mv: set owner/group: Operation not permitted). + mv -f -- "$temp" "${_ARKENFOX_PROFILE_PREFSJS:?}" 2>/dev/null + fi +} + +arkenfox_prefs_cleaner_help() { + cat >&2 <<'EOF' + +This script creates a backup of your prefs.js file before doing anything. +It should be safe, but you can follow these steps if something goes wrong: + +1. Make sure Firefox is closed. +2. Delete prefs.js in your profile folder. +3. Delete Invalidprefs.js if you have one in the same folder. +4. Rename or copy your latest backup to prefs.js. +5. Run Firefox and see if you notice anything wrong with it. +6. If you do notice something wrong, especially with your extensions, and/or with the UI, + go to about:support, and restart Firefox with add-ons disabled. + Then, restart it again normally, and see if the problems were solved. + If you are able to identify the cause of your issues, please bring it up on the arkenfox user.js GitHub repository. + +EOF +} + +# Restore the starting errexit shell option. +is_option_set "${_STARTING_SH_OPTION_ERREXIT?}" || set +o errexit +# "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)." +# shellcheck disable=SC2317 +(main() { :; }) && : For quick navigation in IDEs only. +arkenfox_prefs_cleaner_init && : Suppress errexit if enabled. +status_=$? && + if [ "$status_" -eq 0 ]; then + if test "${_ARKENFOX_PREFS_CLEANER_RUN_NAME:?}" = \ + "${ARKENFOX_PREFS_CLEANER_NAME:?}"; then + arkenfox_prefs_cleaner "$@" + else + print_ok 'The arkenfox prefs.js cleaner script' \ + 'has been successfully sourced.' + print_warning 'If this is not intentional, you may have either' \ + 'made a typo in the shell commands, or renamed this file' \ + 'without defining the environment variable' \ + 'ARKENFOX_PREFS_CLEANER_NAME to match the new name.' \ + " + + Detected name of the run file: ${_ARKENFOX_PREFS_CLEANER_RUN_NAME:?} + ARKENFOX_PREFS_CLEANER_NAME : ${ARKENFOX_PREFS_CLEANER_NAME:?} +" \ + "$(printf '%s\n\b' '')Please note that this is not the" \ + 'expected way to run the arkenfox prefs.js cleaner script.' \ + 'Dot sourcing support is experimental' \ + 'and all function and variable names' \ + 'are still subject to change.' + # Make arkenfox_prefs_cleaner_update_self a no-op as this function + # can not be run reliably when dot sourced. + eval 'arkenfox_prefs_cleaner_update_self() { :; }' && + # Restore the starting aliases. + eval "${_STARTING_ALIASES?}" + fi + else + # Restore the starting aliases. + eval "${_STARTING_ALIASES?}" && + (exit "$status_") + fi diff --git a/updater.sh b/updater.sh index 72c77fcb..846975ce 100755 --- a/updater.sh +++ b/updater.sh @@ -1,407 +1,1387 @@ -#!/usr/bin/env bash - -## arkenfox user.js updater for macOS and Linux - -## version: 4.0 -## Author: Pat Johnson (@overdodactyl) -## Additional contributors: @earthlng, @ema-pe, @claustromaniac, @infinitewarp - -## DON'T GO HIGHER THAN VERSION x.9 !! ( because of ASCII comparison in update_updater() ) - -# Check if running as root -if [ "${EUID:-"$(id -u)"}" -eq 0 ]; then - printf "You shouldn't run this with elevated privileges (such as with doas/sudo).\n" - exit 1 -fi - -readonly CURRDIR=$(pwd) - -SCRIPT_FILE=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null) -[ -z "$SCRIPT_FILE" ] && SCRIPT_FILE=${BASH_SOURCE[0]} -readonly SCRIPT_DIR=$(dirname "${SCRIPT_FILE}") - - -######################### -# Base variables # -######################### - -# Colors used for printing -RED='\033[0;31m' -BLUE='\033[0;34m' -BBLUE='\033[1;34m' -GREEN='\033[0;32m' -ORANGE='\033[0;33m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -# Argument defaults -UPDATE='check' -CONFIRM='yes' -OVERRIDE='user-overrides.js' -BACKUP='multiple' -COMPARE=false -SKIPOVERRIDE=false -VIEW=false -PROFILE_PATH=false -ESR=false - -# Download method priority: curl -> wget -DOWNLOAD_METHOD='' -if command -v curl >/dev/null; then - DOWNLOAD_METHOD='curl --max-redirs 3 -so' -elif command -v wget >/dev/null; then - DOWNLOAD_METHOD='wget --max-redirect 3 --quiet -O' -else - echo -e "${RED}This script requires curl or wget.\nProcess aborted${NC}" - exit 1 -fi - - -show_banner() { - echo -e "${BBLUE} - ############################################################################ - #### #### - #### arkenfox user.js #### - #### Hardening the Privacy and Security Settings of Firefox #### - #### Maintained by @Thorin-Oakenpants and @earthlng #### - #### Updater for macOS and Linux by @overdodactyl #### - #### #### - ############################################################################" - echo -e "${NC}\n" - echo -e "Documentation for this script is available here: ${CYAN}https://github.com/arkenfox/user.js/wiki/5.1-Updater-[Options]#-maclinux${NC}\n" -} - -######################### -# Arguments # -######################### - -usage() { - echo - echo -e "${BLUE}Usage: $0 [-bcdehlnrsuv] [-p PROFILE] [-o OVERRIDE]${NC}" 1>&2 # Echo usage string to standard error - echo -e " -Optional Arguments: +#!/bin/sh + +# arkenfox user.js updater for macOS, Linux and Unix-like operating systems +# authors: @overdodactyl, @earthlng, @9ao9ai9ar +# version: 5.0 + +# IMPORTANT: The version string must be on the 5th line of this file +# and must be of the format "version: MAJOR.MINOR" (spaces are optional). +# This restriction is set by the function arkenfox_script_version. + +# Example advanced script usage: +# $ yes | tr -d '\n' | env WGET__IMPLEMENTATION=wget ./updater.sh 2>/dev/null +# $ TERM=dumb . ./updater.sh && arkenfox_updater + +# This ShellCheck warning is just noise for those who know what they are doing: +# "Note that A && B || C is not if-then-else. C may run when A is true." +# shellcheck disable=SC2015 + +############################################################################### +#### === Common utility functions === #### +#### Code that is shared between updater.sh and prefsCleaner.sh, inlined #### +#### and duplicated only to maintain the same file count as before. #### +############################################################################### + +# Save the starting errexit shell option for later restoration. +# Note that we do not choose the restoration method of running eval on +# the saved output of `set +o` as that would be problematic because: +# 1. bash turns off the errexit option in command substitutions +# and also does not clear errexit in a command substitution +# if `shopt -s inherit_errexit` is in effect: +# https://unix.stackexchange.com/a/383581. +# 2. oksh fails to restore the errexit option using this method. +# 3. oksh turns off the interactive option in command substitutions, +# and trying to toggle this option results in an error. +# 4. it clutters up the command history with lots of set commands. +# It is also more trouble than it is worth to try to work around +# these limitations just to restore one shell option we may have changed, +# not counting the posix and pipefail options as we want to keep them enabled. +case $- in + *e*) _STARTING_SH_OPTION_ERREXIT=1 ;; + *) _STARTING_SH_OPTION_ERREXIT=0 ;; +esac && { + # Unset function of the same name so that it is not invoked in place of + # the `command` regular built-in utility that we are going to be using: + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_09_01_01. + # In some ksh88 derivatives like the pdksh and XPG4 sh, the exit status + # will not be 0 if the function to unset is not already defined. + # In bash, there is no guarantee that the operation will succeed as: + # 1. readonly functions can not be unset. + # 2. all built-in utilities, including special built-in utilities + # like unset and exit, can be overridden by functions. + # 3. to make matters worse, even the reserved words can be overridden + # by aliases when not in POSIX mode. + # So we have no choice but to trust that the job is done faithfully. + # If secure shell scripting is ever a possibility, it is not found in bash. + \unset -f command && \: Suppress errexit if enabled. + \: Set a zero exit status for the grouping command. +} && + # https://pubs.opengroup.org/onlinepubs/9799919799/utilities/command.html + # The `command` built-in, when used without options, serves two purposes: + # 1. it causes the shell to treat the arguments as a simple command + # that is not subject to alias substitution and shell function lookup. + # 2. it suppresses the special characteristics of the special built-ins, + # so that an error does not cause a non-interactive script to abort. + # Though some shells do not respect point 2 above, + # so a prior test run in a subshell is still required. + # alias/unalias are not implemented in posh, hush and gash, and executing + # either in gash will exit the shell, so a safeguard is needed. + if (\command alias >/dev/null 2>&1) && + (\command unalias -a 2>/dev/null); then + # Save the starting aliases for later restoration, then unset + # all aliases asap as alias substitution occurs right before parsing: + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_03_01. + { + # The `-p` option used by several implementations is absolutely + # required in order to properly save and restore alias definitions. + (\command alias -p >/dev/null 2>&1) && + _STARTING_ALIASES=$(\command alias -p) || + _STARTING_ALIASES= + } && + \command unalias -a + else + _STARTING_ALIASES= + fi && { + # Enable POSIX mode. Needed for yash, as otherwise parsing will fail on + # non-ASCII characters if not supported by LC_CTYPE. + (command set -o posix 2>/dev/null) && command set -o posix + : Set a zero exit status for the grouping command. +} && + # Detect spoofing by external, readonly functions. + set -o errexit +# In case of a variable assignment error (or any other shell error): +# "In all of the cases shown in the table where an interactive shell +# is required not to exit, the shell shall not perform any further processing +# of the command in which the error occurred." +# ―https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_01 +# We are being extra cautious here by checking the exit status +# in a separate command because the words "any further processing of +# the command in which the error occurred" are too vague to be relied on. +# For example, if the following shell script is dot sourced in bash: +# ```sh +# readonly r +# r= || echo continue in OR list +# echo next command +# ``` +# only "next command" is shown in the output, whereas in most other shells +# no output is produced as the processing is halted at the dot source command. +# The behavior gets more complicated when we start using functions; +# add in the plethora of shell features and all consistency is lost. +case $? in + 0) \: ;; + *) + # "The behavior of return when not in a function or dot script + # differs between the System V shell and the KornShell. + # In the System V shell this is an error, + # whereas in the KornShell, the effect is the same as exit." + # ―https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_24_18 + \command return 69 2>/dev/null # Service unavailable. + \exit 69 # Service unavailable. + ;; +esac + +download_file() { # args: url + # The try-finally construct can be implemented as a set of trap commands. + # However, it is notoriously difficult to write them portably and reliably. + # Since mktemp_ creates temporary files that are periodically cleared + # on any sane system, we leave it to the OS or the user + # to do the cleaning themselves for simplicity's sake. + temp=$(mktemp_) && + wget_ "$temp" "$1" >/dev/null 2>&1 && + printf '%s\n' "$temp" || { + print_error "Failed to download file from the URL: $1." + return "${_EX_UNAVAILABLE:?}" + } +} + +# An improvement on the "secure shell script" example demonstrated in +# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html#tag_20_22_17. +init() { + # Unset all functions whose name is found in the intersection of + # the standard utilities defined in POSIX.1-2017 and in POSIX.1-2024. + \set -- admin alias ar asa at awk basename batch bc bg cal cat cd cflow \ + chgrp chmod chown cksum cmp comm command compress cp crontab csplit \ + ctags cut cxref date dd delta df diff dirname du echo ed env ex \ + expand expr false fc fg file find fold fuser gencat get getconf \ + getopts grep hash head iconv id ipcrm ipcs jobs join kill lex link ln \ + locale localedef logger logname lp ls m4 mailx make man mesg mkdir \ + mkfifo more mv newgrp nice nl nm nohup od paste patch pathchk pax pr \ + printf prs ps pwd read renice rm rmdel rmdir sact sccs sed sh sleep \ + sort split strings strip stty tabs tail talk tee test time touch tput \ + tr true tsort tty type ulimit umask unalias uname uncompress unexpand \ + unget uniq unlink uucp uudecode uuencode uustat uux val vi wait wc \ + what who write xargs yacc zcat && { + # If the unset is unsuccessful, there are two known possibilities: + # 1. there are readonly functions (bash). + # 2. at least some in the list are not functions (ksh88). + # We check each in turn using the built-in utility `typeset` that is + # available in these shells to see if there are any defined function. + \unset -f "$@" || + while [ "$#" -gt 0 ]; do + # Evaluates to true if `typeset` is not available. + ! \command -- typeset -f "$1" >/dev/null && + \shift || { + # Reset $# to 0 to break the loop as $1 is a function. + \set -- + # Note that this command is not affected by errexit. + ! \: Set a non-zero exit status for the while loop. + } + done + } && + if (\unalias -a 2>/dev/null); then + # It is already too late to run the unalias command at this stage, + # but might still be useful in the case the script is dot sourced, + # acting as a reset mechanism. + \unalias -a + fi && + LC_ALL=C && + # To prevent the accidental insertion of SGR commands in grep's output, + # even when not directed at a terminal, we explicitly set + # the following three environment variables: + GREP_COLORS='mt=:ms=:mc=:sl=:cx=:fn=:ln=:bn=:se=' && + GREP_COLOR='0' && + GREP_OPTIONS= && + export LC_ALL GREP_COLORS GREP_COLOR GREP_OPTIONS && { + path=$(command -p getconf PATH 2>/dev/null) && + PATH="$path:$PATH" && + export PATH || + test "$?" -eq 127 # getconf: command not found (Haiku). + } && { + # The pipefail option was added in POSIX.1-2024 (SUSv5), + # and has long been supported by most major POSIX-compatible shells, + # with the notable exceptions of dash and ksh88-based shells. + # There are some caveats to switching on this option though: + # https://mywiki.wooledge.org/BashPitfalls#set_-euo_pipefail. + (command set -o pipefail 2>/dev/null) && + command set -o pipefail || + : Do without. + } && { + # In XPG4 sh, `unset -f '['` is an error. + # In bash 3, `command` always exits the shell on failure + # when errexit is on, even if guarded by AND/OR lists. + (unset -f '[') 2>/dev/null && + unset -f '[' 2>/dev/null || + command -V '[' | { ! grep -q function; } + } && + IFS=$(printf '%b' ' \n\t') && + umask 0077 && # cp/mv need execute access to parent directories. + # Inspired by https://stackoverflow.com/q/1101957. + exit_status_definitions() { + cut -d'#' -f1 <<'EOF' +_EX_OK=0 # Successful exit status. +_EX_FAIL=1 # Failed exit status. +_EX_USAGE=2 # Command line usage error. +_EX__BASE=64 # Base value for error messages. +_EX_DATAERR=65 # Data format error. +_EX_NOINPUT=66 # Cannot open input. +_EX_NOUSER=67 # Addressee unknown. +_EX_NOHOST=68 # Host name unknown. +_EX_UNAVAILABLE=69 # Service unavailable. +_EX_SOFTWARE=70 # Internal software error. +_EX_OSERR=71 # System error (e.g., can't fork). +_EX_OSFILE=72 # Critical OS file missing. +_EX_CANTCREAT=73 # Can't create (user) output file. +_EX_IOERR=74 # Input/output error. +_EX_TEMPFAIL=75 # Temp failure; user is invited to retry. +_EX_PROTOCOL=76 # Remote error in protocol. +_EX_NOPERM=77 # Permission denied. +_EX_CONFIG=78 # Configuration error. +_EX_NOEXEC=126 # A file to be executed was found, but it was not an executable utility. +_EX_CNF=127 # A utility to be executed was not found. +EOF + } && + exit_status_definitions >/dev/null || { + echo 'Failed to initialize the environment.' >&2 + return 69 # Service unavailable. + } + name= && status_= || return + while IFS='= ' read -r name status_; do + # "When reporting the exit status with the special parameter '?', + # the shell shall report the full eight bits of exit status available." + # ―https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02 + # "The exit status shall be n, if specified, except that + # the behavior is unspecified if n is not an unsigned decimal integer + # or is greater than 255." + # ―https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_21_14 + is_integer "$status_" && + [ "$status_" -ge 0 ] && [ "$status_" -le 255 ] || { + printf '%s %s\n' 'Undefined exit status in the definition:' \ + "$name=$status_." >&2 + return 70 # Internal software error. + } + (eval "$name=" 2>/dev/null) && + eval "$name=$status_" && + eval readonly "$name" || { + eval [ "\"\$$name\"" = "$status_" ] && + continue # $name is already readonly and set to $status_. + printf '%s %s\n' \ + "Failed to assign $status_ to $name and make $name readonly." \ + 'Try again in a new shell environment?' >&2 + return 75 # Temp failure. + } + done <&2 +} + +print_info() { # args: [string ...] + printf '%b' "$*" >&2 +} + +print_missing() { # args: [string ...] + print_error "Failed to find the following utilities on your system: $*." + return "${_EX_CNF:?}" +} + +print_ok() { # args: [string ...] + printf '%s\n' "${_TPUT_AF_GREEN?}OK: $*${_TPUT_SGR0?}" >&2 +} + +print_warning() { # args: [string ...] + printf '%s\n' "${_TPUT_AF_YELLOW?}WARNING: $*${_TPUT_SGR0?}" >&2 +} + +print_yN() { # args: [string ...] + printf '%s' "${_TPUT_AF_RED?}$* [y/N]${_TPUT_SGR0?} " >&2 +} + +probe_mktemp_() { + mktemp_() { + case ${MKTEMP__IMPLEMENTATION?} in + mktemp) command mktemp ;; + m4) + # Copied from https://unix.stackexchange.com/a/181996. + echo 'mkstemp(template)' | + m4 -D template="${TMPDIR:-/tmp}/baseXXXXXX" + ;; + *) print_missing mktemp m4 ;; + esac + } || return + util= && set -- mktemp m4 || return + for util in "$@"; do + if command -v -- "$util" >/dev/null; then + [ "${MKTEMP__IMPLEMENTATION:-"$util"}" != "$util" ] || { + MKTEMP__IMPLEMENTATION=$util + return + } + fi + done + print_missing "${MKTEMP__IMPLEMENTATION:-"$@"}" +} + +probe_open_() { + open_() { # args: file ... + if [ "$#" -eq 0 ]; then + echo 'open_: missing operand' >&2 + return "${_EX_USAGE:?}" + else + return_status=${_EX_OK:?} || return + while [ "$#" -gt 0 ]; do + case ${OPEN__IMPLEMENTATION?} in + xdg-open | open) + # Do not add --, as xdg-open does not conform to the + # XBD Utility Syntax Guidelines. + command "$OPEN__IMPLEMENTATION" "$1" + ;; + firefox) + # Do not add --, as firefox does not conform to the + # XBD Utility Syntax Guidelines. + command firefox "$@" + return + ;; + *) + print_missing xdg-open open firefox + return + ;; + esac + status_=$? || return + [ "$status_" -eq "${_EX_OK:?}" ] || return_status=$status_ + shift + done + return "$return_status" + fi + } || return + util= && set -- xdg-open open firefox || return + for util in "$@"; do + if command -v -- "$util" >/dev/null; then + [ "${OPEN__IMPLEMENTATION:-"$util"}" != "$util" ] || { + OPEN__IMPLEMENTATION=$util + return + } + fi + done + print_missing "${OPEN__IMPLEMENTATION:-"$@"}" +} + +probe_realpath_() { + # Adjusted from https://github.com/ko1nksm/readlinkf. + # Limitation: `readlinkf` cannot handle filenames that end with a newline. + # Execute in a subshell to localize variables and the effect of cd. + readlinkf() ( # args: file + [ "${1:-}" ] || return "${_EX_FAIL:?}" + # The maximum depth of symbolic links is 40. + # This value is the same as defined in the Linux 5.6 kernel. + # However, `readlink -f` has no such limitation. + max_symlinks=40 + CDPATH= # To avoid changing to an unexpected directory. + target=$1 + [ -e "${target%/}" ] || + target=${1%"${1##*[!/]}"} # Trim trailing slashes. + [ -d "${target:-/}" ] && target="$target/" + cd -P . 2>/dev/null || return "${_EX_FAIL:?}" + while [ "$max_symlinks" -ge 0 ] && + max_symlinks=$((max_symlinks - 1)); do + if [ ! "$target" = "${target%/*}" ]; then + case $target in + /*) cd -P "${target%/*}/" 2>/dev/null || break ;; + *) cd -P "./${target%/*}" 2>/dev/null || break ;; + esac + target=${target##*/} + fi + if [ ! -L "$target" ]; then + target="${PWD%/}${target:+/}${target}" + printf '%s\n' "${target:-/}" + return "${_EX_OK:?}" + fi + # `ls -dl` format: "%s %u %s %s %u %s %s -> %s\n", + # , , , , + # , , , + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ls.html + link=$(ls -dl -- "$target" 2>/dev/null) || break + target=${link#*" $target -> "} + done + return "${_EX_FAIL:?}" + ) || return + realpath_() { # args: file ... + if [ "$#" -eq 0 ]; then + echo 'realpath_: missing operand' >&2 + return "${_EX_USAGE:?}" + else + return_status=${_EX_OK:?} || return + while [ "$#" -gt 0 ]; do + case ${REALPATH__IMPLEMENTATION?} in + realpath) command realpath -- "$1" ;; + readlink) command readlink -f -- "$1" ;; + grealpath) command grealpath -- "$1" ;; + greadlink) command greadlink -f -- "$1" ;; + *) readlinkf "$1" ;; + esac + status_=$? || return + [ "$status_" -eq "${_EX_OK:?}" ] || return_status=$status_ + shift + done + return "$return_status" + fi + } || return + # Both realpath and readlink -f as found on the BSDs are quite different + # from their Linux counterparts and even among themselves, + # instead behaving similarly to the POSIX realpath -e for the most part. + # The table below details the varying behaviors where the non-header cells + # note the exit status followed by any output in parentheses: + # | | realpath nosuchfile | realpath nosuchtarget | readlink -f nosuchfile | readlink -f nosuchtarget | + # |---------------|---------------------|-----------------------|------------------------|--------------------------| + # | FreeBSD 14.2 | 1 (error message) | 1 (error message) | 1 | 1 (fully resolved path) | + # | OpenBSD 7.6 | 1 (error message) | 1 (error message) | 1 (error message) | 1 (error message) | + # | NetBSD 10.0 | 0 | 0 | 1 | 1 (fully resolved path) | + # | DragonFly 6.4 | 1 (error message) | 1 (error message) | 1 | 1 (input path argument) | + # It is also worth pointing out that the BusyBox (v1.37.0) + # realpath and readlink -f exit with status 1 without outputting + # the fully resolved path if the argument contains no slash characters + # and does not name a file in the current directory. + name=$(uname) && + case $name in + NetBSD) : ;; # NetBSD realpath works as intended. + Darwin | *BSD | DragonFly) # Other BSDs and macOS should use readlinkf. + REALPATH__IMPLEMENTATION=${REALPATH__IMPLEMENTATION:-readlinkf} + ;; + esac || + return + util= && set -- realpath readlink greadlink || return + for util in "$@"; do + if case $util in + readlink | greadlink) command "$util" -f -- . >/dev/null 2>&1 ;; + *) command "$util" -- . >/dev/null 2>&1 ;; + esac then + [ "${REALPATH__IMPLEMENTATION:-"$util"}" != "$util" ] || { + REALPATH__IMPLEMENTATION=$util + return + } + fi + done + REALPATH__IMPLEMENTATION=readlinkf +} + +# https://mywiki.wooledge.org/BashFAQ/037 +probe_terminal() { + # Testing for multiple terminal capabilities at once is unreliable, + # and the non-POSIX option -S is not recognized by NetBSD's tput, + # which also requires a numerical argument after setaf/AF, + # so we test thus, trying both terminfo and termcap names just in case: + if [ -t 2 ]; then + tput setaf 0 >/dev/null 2>&1 && + tput bold >/dev/null 2>&1 && + tput sgr0 >/dev/null 2>&1 && + _TPUT_AF_RED=$(tput setaf 1) && + _TPUT_AF_GREEN=$(tput setaf 2) && + _TPUT_AF_YELLOW=$(tput setaf 3) && + _TPUT_AF_BLUE=$(tput setaf 4) && + _TPUT_AF_CYAN=$(tput setaf 6) && + _TPUT_BOLD_AF_BLUE=$(tput bold)$(tput setaf 4) && + _TPUT_SGR0=$(tput sgr0) && + return + tput AF 0 >/dev/null 2>&1 && + tput md >/dev/null 2>&1 && + tput me >/dev/null 2>&1 && + _TPUT_AF_RED=$(tput AF 1) && + _TPUT_AF_GREEN=$(tput AF 2) && + _TPUT_AF_YELLOW=$(tput AF 3) && + _TPUT_AF_BLUE=$(tput AF 4) && + _TPUT_AF_CYAN=$(tput AF 6) && + _TPUT_BOLD_AF_BLUE=$(tput md)$(tput AF 4) && + _TPUT_SGR0=$(tput me) && + return + fi + _TPUT_AF_RED= && + _TPUT_AF_GREEN= && + _TPUT_AF_YELLOW= && + _TPUT_AF_BLUE= && + _TPUT_AF_CYAN= && + _TPUT_BOLD_AF_BLUE= && + _TPUT_SGR0= +} + +probe_wget_() { + wget_() { # args: file url + case ${WGET__IMPLEMENTATION?} in + curl) + status_=$( + command curl -sSfw '%{http_code}' -o "$1" -- "$2" + ) && + is_integer "$status_" && + [ "$status_" -ge 200 ] && [ "$status_" -lt 300 ] + ;; + wget) command wget -O "$1" -- "$2" ;; + fetch) command fetch -o "$1" -- "$2" ;; + ftp) command ftp -o "$1" -- "$2" ;; # Progress meter to stdout. + *) print_missing curl wget fetch ftp ;; + esac + } || return + util= && set -- curl wget fetch ftp || return + for util in "$@"; do + if command -v -- "$util" >/dev/null; then + [ "${WGET__IMPLEMENTATION:-"$util"}" != "$util" ] || { + WGET__IMPLEMENTATION=$util + return + } + fi + done + print_missing "${WGET__IMPLEMENTATION:-"$@"}" +} + +# Copied from https://unix.stackexchange.com/a/464963. +read1() { # args: name + if [ -t 0 ]; then + # If stdin is a tty device, put it out of icanon, + # set min and time to sane values, but do not otherwise + # touch other inputs or local settings (echo, isig, icrnl...). + # Take a backup of the previous settings beforehand. + saved_tty_settings=$(stty -g) + stty -icanon min 1 time 0 + fi + eval "$1=" + while + # Read one byte, using a workaround for the fact that + # command substitution strips trailing newline characters. + c=$( + dd bs=1 count=1 2>/dev/null + echo . + ) + c=${c%.} + # Break out of the loop on empty input (eof) + # or if a full character has been accumulated in the output variable + # (using `wc -m` to count the number of characters). + [ -n "$c" ] && + eval "$1=\${$1}"'$c + [ "$(($(printf %s "${'"$1"'}" | wc -m)))" -eq 0 ]' + do + continue + done + if [ -t 0 ]; then + # Restore settings saved earlier if stdin is a tty device. + stty "$saved_tty_settings" + fi +} + +arkenfox_check_nonroot() { + name=$(uname) || return + # Haiku is a single-user operating system. + [ "$name" != 'Haiku' ] || return "${_EX_OK:?}" + id=$(id -u) || return + if is_integer "$id" && [ "$id" -eq 0 ]; then + print_error "You shouldn't run this with elevated privileges" \ + '(such as with doas/sudo).' + return "${_EX_USAGE:?}" + fi +} + +# https://searchfox.org/mozilla-central/source/modules/libpref/parser/src/lib.rs +# user.js supports C, C++ and Python style comments. +# Currently, only the first two forms of comments are handled in this function. +arkenfox_remove_userjs_comments() { # args: file + # Copied in full from the public domain sed script at + # https://sed.sourceforge.io/grabbag/scripts/remccoms3.sed, + # patched to eliminate any unbalanced parenthesis or quotation mark in + # here-documents, comments, or case statement patterns, + # as oksh mishandles them inside the $() form of command substitution + # (using the `` form is not an option as that introduces other errors): + # https://www.gnu.org/savannah-checkouts/gnu/autoconf/manual/html_node/Shell-Substitutions.html#index-_0024_0028commands_0029 + # (see also https://unix.stackexchange.com/q/340923). + # The best POSIX solution on the internet, though it does not handle some + # edge cases as well as Emacs and cpp do; e.g. compare the output of + # `cpp -P -std=c99 -fpreprocessed -undef -dD "$1"` + # (the options "-Werror -Wfatal-errors" could also be added, + # which may mimic Firefox's parsing of user.js better) + # with that of `arkenfox_remove_userjs_comments "$1"`, where the content of + # the input file $1 is the text in the here-document below: + : Unterminated multi-line strings test case <<'EOF' +/* "not/here +*/"//" +// non "here /* +should/appear +// \ +nothere +should/appear +"a \" string with embedded comment /* // " /*nothere*/ +"multiline +/*string" /**/ shouldappear //*nothere*/ +/*/ nothere*/ should appear +EOF + remccoms3=$( + # Apparently, the redirection operator "<<", but not "<<-", here + # inside a command substitution breaks the syntax highlighting and the + # functions outline in the structure tool window of JetBrains IDEs. + cat <<-'EOF' +#! /bin/sed -nf + +# Remove C and C++ comments, by Brian Hiles (brian_hiles@rocketmail.com) + +# Sped up (and bugfixed to some extent) by Paolo Bonzini (bonzini@gnu.org) +# Works its way through the line, copying to hold space the text up to the +# first special character (/, '"', "'"). The original version went exactly a +# character at a time, hence the greater speed of this one. But the concept +# and especially the trick of building the line in hold space are entirely +# merit of Brian. + +:loop + +# This line is sufficient to remove C++ comments! +/^\/\// s,.*,, + +/^$/{ + x + p + n + b loop +} +/^"/{ + :double + /^$/{ + x + p + n + /^"/b break + b double + } + + H + x + s,\n\(.[^\"]*\).*,\1, + x + s,.[^\"]*,, + + /^"/b break + /^\\/{ + H + x + s,\n\(.\).*,\1, + x + s/.// + } + b double +} + +/^'/{ + :single + /^$/{ + x + p + n + /^'/b break + b single + } + H + x + s,\n\(.[^\']*\).*,\1, + x + s,.[^\']*,, + + /^'/b break + /^\\/{ + H + x + s,\n\(.\).*,\1, + x + s/.// + } + b single +} + +/^\/\*/{ + s/.// + :ccom + s,^.[^*]*,, + /^$/ n + /^\*\//{ + s/..// + b loop + } + b ccom +} + +:break +H +x +s,\n\(.[^"'/]*\).*,\1, +x +s/.[^"'/]*// +b loop +EOF + ) && + # A case of indefinite loop is prevented by setting LC_ALL=C in init: + # https://stackoverflow.com/q/13061785/#comment93013794_13062074. + sed -n -- "$remccoms3" "$1" | + sed '/^[[:space:]]*$/d' # Remove blank lines. +} + +arkenfox_script_version() { # args: file + # Character classes and range expressions are locale-dependent: + # https://unix.stackexchange.com/a/654391. + format='[0123456789]\{1,\}\.[0123456789]\{1,\}' && + version=$( + sed -n -- "5s/.*version:[[:blank:]]*\($format\).*/\1/p" "$1" + ) && + [ -n "$version" ] && + printf '%s\n' "$version" || { + print_error "Failed to determine the version of the script file: $1." + return "${_EX_DATAERR:?}" + } +} + +arkenfox_select_firefox_profile() { # args: file + while :; do + # Adapted from https://unix.stackexchange.com/a/786827. + profiles=$( + # Character classes and range expressions are locale-dependent: + # https://unix.stackexchange.com/a/654391. + awk -- '/^[[]/ { section = substr($0, 1) } + (section ~ /^[[]Profile[0123456789]+[]]$/) { print }' "$1" + ) && + profile_count=$( + printf '%s' "$profiles" | + grep -Ec '^[[]Profile[0123456789]+[]]$' + ) && + is_integer "$profile_count" && [ "$profile_count" -gt 0 ] || { + print_error 'Failed to find the profile sections in the INI file.' + return "${_EX_DATAERR:?}" + } + if [ "$profile_count" -eq 1 ]; then + printf '%s\n' "$profiles" + return + else + profiles_display=$( + printf '%s\n\n' "$profiles" | + grep -Ev -e '^IsRelative=' -e '^Default=' && + awk -- '/^[[]/ { section = substr($0, 2) } + ((section ~ /^Install/) && /^Default=/) \ + { print }' "$1" + ) || return + cat >&2 </dev/null # Service unavailable. + \exit 69 # Service unavailable. + ;; +esac + +arkenfox_updater_init() { + probe_terminal && probe_realpath_ || return + # IMPORTANT: ARKENFOX_UPDATER_NAME must be synced to the name of this file! + # This is so that we may somewhat determine if the script is sourced or not + # by comparing it to the basename of the canonical path of $0, + # which should be better than hard coding all the names of + # the interactive and non-interactive POSIX shells in existence. + # Cf. https://stackoverflow.com/a/28776166. + ARKENFOX_UPDATER_NAME=${ARKENFOX_UPDATER_NAME:-updater.sh} || return + (_ARKENFOX_REPO_DOWNLOAD_URL_ROOT=) 2>/dev/null && + _ARKENFOX_REPO_DOWNLOAD_URL_ROOT='https://raw.githubusercontent.com/arkenfox/user.js/master' && + readonly _ARKENFOX_REPO_DOWNLOAD_URL_ROOT || + test "$_ARKENFOX_REPO_DOWNLOAD_URL_ROOT" = \ + 'https://raw.githubusercontent.com/arkenfox/user.js/master' || { + print_error 'Failed to assign the arkenfox repository download URL' \ + 'and make it readonly. Try again in a new shell environment?' + return "${_EX_TEMPFAIL:?}" + } + path=$(realpath_ "$0") && + dir_name=$(dirname -- "$path") && + base_name=$(basename -- "$path") || { + print_error 'Failed to resolve the run file path.' + return "${_EX_UNAVAILABLE:?}" + } + ( + _ARKENFOX_UPDATER_RUN_PATH= && + _ARKENFOX_UPDATER_RUN_DIR= && + _ARKENFOX_UPDATER_RUN_NAME= + ) 2>/dev/null && + _ARKENFOX_UPDATER_RUN_PATH=$path && + _ARKENFOX_UPDATER_RUN_DIR=$dir_name && + _ARKENFOX_UPDATER_RUN_NAME=$base_name && + readonly _ARKENFOX_UPDATER_RUN_PATH \ + _ARKENFOX_UPDATER_RUN_DIR \ + _ARKENFOX_UPDATER_RUN_NAME || { + [ "$_ARKENFOX_UPDATER_RUN_PATH" = "$path" ] && + [ "$_ARKENFOX_UPDATER_RUN_DIR" = "$dir_name" ] && + [ "$_ARKENFOX_UPDATER_RUN_NAME" = "$base_name" ] || { + print_error 'Failed to make the resolved run file path readonly.' \ + 'Try again in a new shell environment?' + return "${_EX_TEMPFAIL:?}" + } + } +} + +arkenfox_updater() { # args: [option ...] + arkenfox_updater_parse_options "$@" && + arkenfox_updater_probe_utilities || + return + arkenfox_updater_exec_general_options || { + status_=$? || return + # An exit status of _EX__BASE indicates that a command tied to + # a general option has been executed successfully. + [ "$status_" -eq "${_EX__BASE:?}" ] && + return "${_EX_OK:?}" || + return "$status_" + } + arkenfox_check_nonroot && + arkenfox_updater_update_self "$@" && + arkenfox_updater_set_profile_path && + arkenfox_updater_check_no_root_owned_files && + arkenfox_updater_banner && + arkenfox_updater_update_userjs +} + +arkenfox_updater_usage() { + cat >&2 </dev/null && echo "$tf" || echo '' # return the temp-filename or empty string on error -} - -open_file() { # expects one argument: file_path - if [ "$(uname)" == 'Darwin' ]; then - open "$1" - elif [ "$(uname -s | cut -c -5)" == "Linux" ]; then - xdg-open "$1" - else - echo -e "${RED}Error: Sorry, opening files is not supported for your OS.${NC}" - fi -} - -readIniFile() { # expects one argument: absolute path of profiles.ini - declare -r inifile="$1" - - # tempIni will contain: [ProfileX], Name=, IsRelative= and Path= (and Default= if present) of the only (if) or the selected (else) profile - if [ "$(grep -c '^\[Profile' "${inifile}")" -eq "1" ]; then ### only 1 profile found - tempIni="$(grep '^\[Profile' -A 4 "${inifile}")" - else - echo -e "Profiles found:\n––––––––––––––––––––––––––––––" - ## cmd-substitution to strip trailing newlines and in quotes to keep internal ones: - echo "$(grep --color=never -E 'Default=[^1]|\[Profile[0-9]*\]|Name=|Path=|^$' "${inifile}")" - echo '––––––––––––––––––––––––––––––' - read -p 'Select the profile number ( 0 for Profile0, 1 for Profile1, etc ) : ' -r - echo -e "\n" - if [[ $REPLY =~ ^(0|[1-9][0-9]*)$ ]]; then - tempIni="$(grep "^\[Profile${REPLY}" -A 4 "${inifile}")" || { - echo -e "${RED}Profile${REPLY} does not exist!${NC}" && exit 1 - } - else - echo -e "${RED}Invalid selection!${NC}" && exit 1 - fi - fi - # extracting 0 or 1 from the "IsRelative=" line - declare -r pathisrel=$(sed -n 's/^IsRelative=\([01]\)$/\1/p' <<< "${tempIni}") +EOF +} - # extracting only the path itself, excluding "Path=" - PROFILE_PATH=$(sed -n 's/^Path=\(.*\)$/\1/p' <<< "${tempIni}") - # update global variable if path is relative - [[ ${pathisrel} == "1" ]] && PROFILE_PATH="$(dirname "${inifile}")/${PROFILE_PATH}" +arkenfox_updater_parse_options() { # args: [option ...] + name= && + # OPTIND must be manually reset between multiple calls to getopts. + OPTIND=1 && + _ARKENFOX_UPDATER_OPTIONS_DISJOINT=0 && + _ARKENFOX_UPDATER_OPTION_H_HELP= && + _ARKENFOX_UPDATER_OPTION_R_REVIEW_ONLY= && + _ARKENFOX_UPDATER_OPTION_D_DONT_UPDATE= && + _ARKENFOX_UPDATER_OPTION_U_UPDATER_SILENT= && + _ARKENFOX_UPDATER_OPTION_P_PROFILE_PATH= && + _ARKENFOX_UPDATER_OPTION_L_LIST_PROFILES= && + _ARKENFOX_UPDATER_OPTION_S_SILENT= && + _ARKENFOX_UPDATER_OPTION_C_COMPARE= && + _ARKENFOX_UPDATER_OPTION_B_BACKUP_SINGLE= && + _ARKENFOX_UPDATER_OPTION_E_ESR= && + _ARKENFOX_UPDATER_OPTION_N_NO_OVERRIDES= && + _ARKENFOX_UPDATER_OPTION_O_OVERRIDES= && + _ARKENFOX_UPDATER_OPTION_V_VIEW= || + return + while getopts 'hrdup:lscbeno:v' name; do + ! is_option_set "$_ARKENFOX_UPDATER_OPTIONS_DISJOINT" || { + arkenfox_updater_usage + return "${_EX_USAGE:?}" + } + case $name in + # General options + h) + _ARKENFOX_UPDATER_OPTION_H_HELP=1 + _ARKENFOX_UPDATER_OPTIONS_DISJOINT=1 + ;; + r) + _ARKENFOX_UPDATER_OPTION_R_REVIEW_ONLY=1 + _ARKENFOX_UPDATER_OPTIONS_DISJOINT=1 + ;; + # Updater options + d) _ARKENFOX_UPDATER_OPTION_D_DONT_UPDATE=1 ;; + u) _ARKENFOX_UPDATER_OPTION_U_UPDATER_SILENT=1 ;; + # user.js options + p) _ARKENFOX_UPDATER_OPTION_P_PROFILE_PATH=$OPTARG ;; + l) _ARKENFOX_UPDATER_OPTION_L_LIST_PROFILES=1 ;; + s) _ARKENFOX_UPDATER_OPTION_S_SILENT=1 ;; + c) _ARKENFOX_UPDATER_OPTION_C_COMPARE=1 ;; + b) _ARKENFOX_UPDATER_OPTION_B_BACKUP_SINGLE=1 ;; + e) _ARKENFOX_UPDATER_OPTION_E_ESR=1 ;; + n) _ARKENFOX_UPDATER_OPTION_N_NO_OVERRIDES=1 ;; + o) _ARKENFOX_UPDATER_OPTION_O_OVERRIDES=$OPTARG ;; + v) _ARKENFOX_UPDATER_OPTION_V_VIEW=1 ;; + \?) + arkenfox_updater_usage + return "${_EX_USAGE:?}" + ;; + :) return "${_EX_USAGE:?}" ;; + esac + done } -getProfilePath() { - declare -r f1=~/Library/Application\ Support/Firefox/profiles.ini - declare -r f2=~/.mozilla/firefox/profiles.ini +arkenfox_updater_probe_utilities() { + ! is_option_set "${_ARKENFOX_UPDATER_OPTION_H_HELP?}" || + return "${_EX_OK:?}" + probe_mktemp_ && + probe_wget_ && + if is_option_set "${_ARKENFOX_UPDATER_OPTION_R_REVIEW_ONLY?}" || + is_option_set "${_ARKENFOX_UPDATER_OPTION_V_VIEW?}"; then + probe_open_ + fi +} - if [ "$PROFILE_PATH" = false ]; then - PROFILE_PATH="$SCRIPT_DIR" - elif [ "$PROFILE_PATH" = 'list' ]; then - if [[ -f "$f1" ]]; then - readIniFile "$f1" # updates PROFILE_PATH or exits on error - elif [[ -f "$f2" ]]; then - readIniFile "$f2" +arkenfox_updater_exec_general_options() { + if is_option_set "${_ARKENFOX_UPDATER_OPTION_H_HELP?}"; then + arkenfox_updater_usage 2>&1 + elif is_option_set "${_ARKENFOX_UPDATER_OPTION_R_REVIEW_ONLY?}"; then + arkenfox_updater_review_userjs + else + return "${_EX_OK:?}" + fi + # We want to return from the caller function as well + # if a command tied to a general option is executed. + # To achieve that, we translate an exit status of _EX_OK to _EX__BASE + # and handle the retranslation back to its original exit status + # in the caller function. + status_=$? || return + if [ "$status_" -eq "${_EX_OK:?}" ]; then + return "${_EX__BASE:?}" + # If the command returns with exit status _EX__BASE (64), + # we change it to an exit status unused by all of curl, wget and fetch, + # which happens to be the meaningful _EX_PROTOCOL (76): + # https://everything.curl.dev/cmdline/exitcode.html + # https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html + # https://man.freebsd.org/cgi/man.cgi?query=fetch&apropos=0&sektion=1&manpath=FreeBSD+14.2-RELEASE&arch=default&format=html#EXIT_STATUS + elif [ "$status_" -eq "${_EX__BASE:?}" ]; then + return "${_EX_PROTOCOL:?}" else - echo -e "${RED}Error: Sorry, -l is not supported for your OS${NC}" - exit 1 + return "$status_" fi - #else - # PROFILE_PATH already set by user with -p - fi } -######################### -# Update updater.sh # -######################### +arkenfox_updater_review_userjs() { + temp=$(download_file "${_ARKENFOX_REPO_DOWNLOAD_URL_ROOT:?}/user.js") && + downloaded_file=$temp.js && + # Suppress diagnostic message on FreeBSD/DragonFly + # (mv: set owner/group: Operation not permitted). + mv -f -- "$temp" "$downloaded_file" 2>/dev/null && + print_ok 'user.js was saved to the temporary file:' \ + "$downloaded_file." && + open_ "$downloaded_file" +} -# Returns the version number of a updater.sh file -get_updater_version() { - echo "$(sed -n '5 s/.*[[:blank:]]\([[:digit:]]*\.[[:digit:]]*\)/\1/p' "$1")" +arkenfox_updater_update_self() { # args: [option ...] + ! is_option_set "${_ARKENFOX_UPDATER_OPTION_D_DONT_UPDATE?}" || + return "${_EX_OK:?}" + downloaded_file=$( + download_file "${_ARKENFOX_REPO_DOWNLOAD_URL_ROOT:?}/updater.sh" + ) || return + local_version=$( + arkenfox_script_version "${_ARKENFOX_UPDATER_RUN_PATH:?}" + ) && + downloaded_version=$(arkenfox_script_version "$downloaded_file") && + local_version_major=${local_version%%.*} && + is_integer "$local_version_major" && + local_version_minor=${local_version#*.} && + is_integer "$local_version_minor" && + downloaded_version_major=${downloaded_version%%.*} && + is_integer "$downloaded_version_major" && + downloaded_version_minor=${downloaded_version#*.} && + is_integer "$downloaded_version_minor" || { + print_error 'Failed to obtain valid version parts for comparison.' + return "${_EX_DATAERR:?}" + } + if [ "$local_version_major" -eq "$downloaded_version_major" ] && + [ "$local_version_minor" -lt "$downloaded_version_minor" ] || + [ "$local_version_major" -lt "$downloaded_version_major" ]; then + if ! is_option_set \ + "${_ARKENFOX_UPDATER_OPTION_U_UPDATER_SILENT?}"; then + print_info 'There is a newer version of updater.sh available. ' + print_yN 'Update and execute?' + read1 REPLY || return + print_info '\n\n' + [ "$REPLY" = 'Y' ] || [ "$REPLY" = 'y' ] || return "${_EX_OK:?}" + fi + # Suppress diagnostic message on FreeBSD/DragonFly + # (mv: set owner/group: Operation not permitted). + mv -f -- \ + "$downloaded_file" \ + "${_ARKENFOX_UPDATER_RUN_PATH:?}" 2>/dev/null && + chmod -- u+rx "${_ARKENFOX_UPDATER_RUN_PATH:?}" || { + print_error 'Failed to update the arkenfox user.js updater' \ + 'and make it executable.' + return "${_EX_CANTCREAT:?}" + } + "${_ARKENFOX_UPDATER_RUN_PATH:?}" -d "$@" + fi } -# Update updater.sh -# Default: Check for update, if available, ask user if they want to execute it -# Args: -# -d: New version will not be looked for and update will not occur -# -u: Check for update, if available, execute without asking -update_updater() { - [ "$UPDATE" = 'no' ] && return 0 # User signified not to check for updates +arkenfox_updater_set_profile_path() { + if [ -n "${_ARKENFOX_UPDATER_OPTION_P_PROFILE_PATH?}" ]; then + _ARKENFOX_PROFILE_PATH=${_ARKENFOX_UPDATER_OPTION_P_PROFILE_PATH?} + elif is_option_set "${_ARKENFOX_UPDATER_OPTION_L_LIST_PROFILES?}"; then + _ARKENFOX_PROFILE_PATH=$(arkenfox_select_firefox_profile_path) + else + _ARKENFOX_PROFILE_PATH=${_ARKENFOX_UPDATER_RUN_DIR:?} + fi && + _ARKENFOX_PROFILE_PATH=$(realpath_ "$_ARKENFOX_PROFILE_PATH") || + return + [ -w "$_ARKENFOX_PROFILE_PATH" ] && + cd -- "$_ARKENFOX_PROFILE_PATH" || { + print_error 'The path to your Firefox profile' \ + "('$_ARKENFOX_PROFILE_PATH') failed to be a directory to which" \ + 'the user has both write and execute access.' + return "${_EX_UNAVAILABLE:?}" + } + _ARKENFOX_PROFILE_USERJS=${_ARKENFOX_PROFILE_PATH%/}/user.js && + _ARKENFOX_PROFILE_USERJS_BACKUP_DIR=${_ARKENFOX_PROFILE_PATH%/}/userjs_backups && + _ARKENFOX_PROFILE_USERJS_DIFF_DIR=${_ARKENFOX_PROFILE_PATH%/}/userjs_diffs +} - declare -r tmpfile="$(download_file 'https://raw.githubusercontent.com/arkenfox/user.js/master/updater.sh')" - [ -z "${tmpfile}" ] && echo -e "${RED}Error! Could not download updater.sh${NC}" && return 1 # check if download failed +arkenfox_updater_check_no_root_owned_files() { + root_owned_files=$( + find -- "${_ARKENFOX_PROFILE_PATH:?}" \ + -path "${_ARKENFOX_PROFILE_PATH%/}/*" -prune -user 0 \( \ + -path "${_ARKENFOX_PROFILE_USERJS:?}" \ + -o -path "${_ARKENFOX_PROFILE_USERJS_BACKUP_DIR:?}" \ + -o -path "${_ARKENFOX_PROFILE_USERJS_DIFF_DIR:?}" \ + \) -print + ) && + if [ -n "$root_owned_files" ]; then + # \b is a backspace to keep the trailing newlines + # from being stripped by command substitution. + print_error 'It looks like this script' \ + 'was previously run with elevated privileges.' \ + 'Please change ownership of the following files' \ + 'to your user and try again:' \ + "$(printf '%s\n\b' '')$root_owned_files" + return "${_EX_CONFIG:?}" + fi +} - if [[ $(get_updater_version "$SCRIPT_FILE") < $(get_updater_version "${tmpfile}") ]]; then - if [ "$UPDATE" = 'check' ]; then - echo -e "There is a newer version of updater.sh available. ${RED}Update and execute Y/N?${NC}" - read -p "" -n 1 -r - echo -e "\n\n" - [[ $REPLY =~ ^[Yy]$ ]] || return 0 # Update available, but user chooses not to update - fi - else - return 0 # No update available - fi - mv "${tmpfile}" "$SCRIPT_FILE" - chmod u+x "$SCRIPT_FILE" - "$SCRIPT_FILE" "$@" -d - exit 0 -} - -######################### -# Update user.js # -######################### - -# Returns version number of a user.js file -get_userjs_version() { - [ -e "$1" ] && echo "$(sed -n '4p' "$1")" || echo "Not detected." -} - -add_override() { - input=$1 - if [ -f "$input" ]; then - echo "" >> user.js - cat "$input" >> user.js - echo -e "Status: ${GREEN}Override file appended:${NC} ${input}" - elif [ -d "$input" ]; then - SAVEIFS=$IFS - IFS=$'\n\b' # Set IFS - FILES="${input}"/*.js - for f in $FILES - do - add_override "$f" - done - IFS=$SAVEIFS # restore $IFS - else - echo -e "${ORANGE}Warning: Could not find override file:${NC} ${input}" - fi -} - -remove_comments() { # expects 2 arguments: from-file and to-file - sed -e '/^\/\*.*\*\/[[:space:]]*$/d' -e '/^\/\*/,/\*\//d' -e 's|^[[:space:]]*//.*$||' -e '/^[[:space:]]*$/d' -e 's|);[[:space:]]*//.*|);|' "$1" > "$2" -} - -# Applies latest version of user.js and any custom overrides -update_userjs() { - declare -r newfile="$(download_file 'https://raw.githubusercontent.com/arkenfox/user.js/master/user.js')" - [ -z "${newfile}" ] && echo -e "${RED}Error! Could not download user.js${NC}" && return 1 # check if download failed - - echo -e "Please observe the following information: - Firefox profile: ${ORANGE}$(pwd)${NC} - Available online: ${ORANGE}$(get_userjs_version "$newfile")${NC} - Currently using: ${ORANGE}$(get_userjs_version user.js)${NC}\n\n" - - if [ "$CONFIRM" = 'yes' ]; then - echo -e "This script will update to the latest user.js file and append any custom configurations from user-overrides.js. ${RED}Continue Y/N? ${NC}" - read -p "" -n 1 -r - echo -e "\n" - if ! [[ $REPLY =~ ^[Yy]$ ]]; then - echo -e "${RED}Process aborted${NC}" - rm "$newfile" - return 1 +arkenfox_updater_banner() { + cat >&2 <&2 </dev/null - fi - - # backup user.js - mkdir -p userjs_backups - local bakname="userjs_backups/user.js.backup.$(date +"%Y-%m-%d_%H%M")" - [ "$BACKUP" = 'single' ] && bakname='userjs_backups/user.js.backup' - cp user.js "$bakname" &>/dev/null - - mv "${newfile}" user.js - echo -e "Status: ${GREEN}user.js has been backed up and replaced with the latest version!${NC}" - - if [ "$ESR" = true ]; then - sed -e 's/\/\* \(ESR[0-9]\{2,\}\.x still uses all.*\)/\/\/ \1/' user.js > user.js.tmp && mv user.js.tmp user.js - echo -e "Status: ${GREEN}ESR related preferences have been activated!${NC}" - fi - - # apply overrides - if [ "$SKIPOVERRIDE" = false ]; then - while IFS=',' read -ra FILES; do - for FILE in "${FILES[@]}"; do - add_override "$FILE" - done - done <<< "$OVERRIDE" - fi - - # create diff - if [ "$COMPARE" = true ]; then - pastuserjs='userjs_diffs/past_user.js' - past_nocomments='userjs_diffs/past_userjs.txt' - current_nocomments='userjs_diffs/current_userjs.txt' - - remove_comments "$pastuserjs" "$past_nocomments" - remove_comments user.js "$current_nocomments" - - diffname="userjs_diffs/diff_$(date +"%Y-%m-%d_%H%M").txt" - diff=$(diff -w -B -U 0 "$past_nocomments" "$current_nocomments") - if [ -n "$diff" ]; then - echo "$diff" > "$diffname" - echo -e "Status: ${GREEN}A diff file was created:${NC} ${PWD}/${diffname}" + backup=$(arkenfox_updater_backup_userjs) && + # Suppress diagnostic message on FreeBSD/DragonFly + # (mv: set owner/group: Operation not permitted). + mv -f -- \ + "$downloaded_file" \ + "${_ARKENFOX_PROFILE_USERJS:?}" 2>/dev/null && + print_ok 'user.js has been backed up' \ + 'and replaced with the latest version!' && + arkenfox_updater_customize_userjs && + arkenfox_updater_diff_with_userjs "$backup" || + return + ! is_option_set "${_ARKENFOX_UPDATER_OPTION_V_VIEW?}" || + open_ "${_ARKENFOX_PROFILE_USERJS:?}" +} + +arkenfox_updater_backup_userjs() { + [ -e "${_ARKENFOX_PROFILE_USERJS:?}" ] || { + print_info 'Unable to backup user.js because it does not exist. ' + print_yN 'Continue?' + read1 REPLY || return + print_info '\n\n' + [ "$REPLY" = 'Y' ] || [ "$REPLY" = 'y' ] || return "${_EX_FAIL:?}" + return "${_EX_OK:?}" + } + if is_option_set "${_ARKENFOX_UPDATER_OPTION_B_BACKUP_SINGLE?}"; then + backup=${_ARKENFOX_PROFILE_USERJS_BACKUP_DIR:?}/user.js.backup else - echo -e "Warning: ${ORANGE}Your new user.js file appears to be identical. No diff file was created.${NC}" - [ "$BACKUP" = 'multiple' ] && rm "$bakname" &>/dev/null - fi - rm "$past_nocomments" "$current_nocomments" "$pastuserjs" &>/dev/null - fi - - [ "$VIEW" = true ] && open_file "${PWD}/user.js" -} - -######################### -# Execute # -######################### - -if [ $# != 0 ]; then - # Display usage if first argument is -help or --help - if [ "$1" = '--help' ] || [ "$1" = '-help' ]; then - usage - else - while getopts ":hp:ludsno:bcvre" opt; do - case $opt in - h) - usage - ;; - p) - PROFILE_PATH=${OPTARG} - ;; - l) - PROFILE_PATH='list' - ;; - u) - UPDATE='yes' - ;; - d) - UPDATE='no' - ;; - s) - CONFIRM='no' - ;; - n) - SKIPOVERRIDE=true - ;; - o) - OVERRIDE=${OPTARG} - ;; - b) - BACKUP='single' - ;; - c) - COMPARE=true - ;; - v) - VIEW=true - ;; - e) - ESR=true - ;; - r) - tfile="$(download_file 'https://raw.githubusercontent.com/arkenfox/user.js/master/user.js')" - [ -z "${tfile}" ] && echo -e "${RED}Error! Could not download user.js${NC}" && exit 1 # check if download failed - mv "$tfile" "${tfile}.js" - echo -e "${ORANGE}Warning: user.js was saved to temporary file ${tfile}.js${NC}" - open_file "${tfile}.js" - exit 0 - ;; - \?) - echo -e "${RED}\n Error! Invalid option: -$OPTARG${NC}" >&2 - usage - ;; - :) - echo -e "${RED}Error! Option -$OPTARG requires an argument.${NC}" >&2 - exit 2 - ;; - esac - done - fi -fi + backup=${_ARKENFOX_PROFILE_USERJS_BACKUP_DIR:?} && + backup=${backup%/}/user.js.backup.$(date +"%Y-%m-%d_%H%M") + fi && + # Add the -p option so that mkdir does not return a >0 exit status + # if any of the specified directories already exists. + mkdir -p -- "${_ARKENFOX_PROFILE_USERJS_BACKUP_DIR:?}" && + cp -f -- "${_ARKENFOX_PROFILE_USERJS:?}" "$backup" && + printf '%s\n' "$backup" +} -show_banner -update_updater "$@" +arkenfox_updater_customize_userjs() { + if is_option_set "${_ARKENFOX_UPDATER_OPTION_E_ESR?}"; then + temp=$(mktemp_) || return + # Character classes and range expressions are locale-dependent: + # https://unix.stackexchange.com/a/654391. + sed -- \ + 's/\/\* \(ESR[0123456789]\{2,\}\.x still uses all.*\)/\/\/ \1/' \ + "${_ARKENFOX_PROFILE_USERJS:?}" >|"$temp" && + # Suppress diagnostic message on FreeBSD/DragonFly + # (mv: set owner/group: Operation not permitted). + mv -f -- "$temp" "${_ARKENFOX_PROFILE_USERJS:?}" 2>/dev/null && + print_ok 'ESR related preferences have been activated!' || + return + fi + if ! is_option_set "${_ARKENFOX_UPDATER_OPTION_N_NO_OVERRIDES?}"; then + if [ -n "${_ARKENFOX_UPDATER_OPTION_O_OVERRIDES?}" ]; then + path=${_ARKENFOX_UPDATER_OPTION_O_OVERRIDES?} + else + path=${_ARKENFOX_PROFILE_PATH:?}/user-overrides.js + fi && ( + # "Double quote to prevent globbing and word splitting." + # shellcheck disable=SC2086 + IFS=, && + command set -o noglob && # Not implemented in hush. + arkenfox_updater_append_userjs_overrides $path + ) + fi +} -getProfilePath # updates PROFILE_PATH or exits on error -cd "$PROFILE_PATH" || exit 1 +arkenfox_updater_append_userjs_overrides() { # args: file ... + path= || return + while [ "$#" -gt 0 ]; do + path=$(realpath_ "$1") && + if [ -f "$path" ]; then + # Using an interim temp file ensures that + # the whole overrides file is appended + # and allows appending the user.js file to itself. + # The side effect is that the file permissions of + # the new user.js may be different from before. + temp=$(mktemp_) && + cat -- "${_ARKENFOX_PROFILE_USERJS:?}" >|"$temp" && + echo >>"$temp" && + cat -- "$path" >>"$temp" && + # Suppress diagnostic message on FreeBSD/DragonFly + # (mv: set owner/group: Operation not permitted). + mv -f -- \ + "$temp" \ + "${_ARKENFOX_PROFILE_USERJS:?}" 2>/dev/null && + print_ok "Overrides file appended: $path." || { + print_error "Failed to append overrides file: $path." + return "${_EX_UNAVAILABLE:?}" + } + elif [ -d "$path" ]; then + # Execute in a subshell to localize variables + # and the effect of `set +o noglob`. + ( + command set +o noglob && # Not implemented in hush. + set -- "$path"/*.js && + while [ "$#" -gt 0 ]; do + arkenfox_updater_append_userjs_overrides "$1" || + exit + shift + done + ) || return "${_EX_UNAVAILABLE:?}" + else + ! : + fi || + print_warning "Could not find overrides file: ${path:-$1}." + shift + done +} -# Check if any files have the owner as root/wheel. -if [ -n "$(find ./ -user 0)" ]; then - printf 'It looks like this script was previously run with elevated privileges, -you will need to change ownership of the following files to your user:\n' - find . -user 0 - cd "$CURRDIR" - exit 1 -fi +arkenfox_updater_diff_with_userjs() { # args: file + if is_option_set "${_ARKENFOX_UPDATER_OPTION_C_COMPARE?}"; then + [ -e "$1" ] || { + print_warning "No diff is performed because of missing file: $1." + return "${_EX_OK:?}" + } + else + return "${_EX_OK:?}" + fi + # Add the -p option so that mkdir does not return a >0 exit status + # if any of the specified directories already exists. + mkdir -p -- "${_ARKENFOX_PROFILE_USERJS_DIFF_DIR:?}" && + old_userjs_stripped=$(mktemp_) && + new_userjs_stripped=$(mktemp_) && + arkenfox_remove_userjs_comments "$1" >|"$old_userjs_stripped" && + arkenfox_remove_userjs_comments \ + "${_ARKENFOX_PROFILE_USERJS:?}" >|"$new_userjs_stripped" || + return + # OpenIndiana's diff -U may output "No differences encountered". + diff=$(diff -b -U 0 -- "$old_userjs_stripped" "$new_userjs_stripped") + status_=$? || return + if [ "$status_" -eq "${_EX_OK:?}" ]; then + print_warning 'Your new user.js file appears to be identical.' \ + 'No diff file was created.' + elif [ -n "$diff" ]; then + diff_file=${_ARKENFOX_PROFILE_USERJS_DIFF_DIR:?} && + diff_file=${diff_file%/}/diff_$(date +"%Y-%m-%d_%H%M").txt && + temp=$(mktemp_) && + printf '%s\n' "$diff" | + sed -e "1s|\($old_userjs_stripped\)|\1 (old user.js)|" \ + -e "2s|\($new_userjs_stripped\)|\1 (new user.js)|" \ + >|"$temp" && + # Suppress diagnostic message on FreeBSD/DragonFly + # (mv: set owner/group: Operation not permitted). + mv -f -- "$temp" "$diff_file" 2>/dev/null || + return + print_ok "A diff file was created: $diff_file." + else + return "$status_" + fi + return "${_EX_OK:?}" +} -update_userjs +# Restore the starting errexit shell option. +is_option_set "${_STARTING_SH_OPTION_ERREXIT?}" || set +o errexit +# "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)." +# shellcheck disable=SC2317 +(main() { :; }) && : For quick navigation in IDEs only. +arkenfox_updater_init && : Suppress errexit if enabled. +status_=$? && + if [ "$status_" -eq 0 ]; then + if test "${_ARKENFOX_UPDATER_RUN_NAME:?}" = \ + "${ARKENFOX_UPDATER_NAME:?}"; then + arkenfox_updater "$@" + else + print_ok 'The arkenfox user.js updater script' \ + 'has been successfully sourced.' + print_warning 'If this is not intentional, you may have either' \ + 'made a typo in the shell commands, or renamed this file' \ + 'without defining the environment variable' \ + 'ARKENFOX_UPDATER_NAME to match the new name.' \ + " -cd "$CURRDIR" + Detected name of the run file: ${_ARKENFOX_UPDATER_RUN_NAME:?} + ARKENFOX_UPDATER_NAME : ${ARKENFOX_UPDATER_NAME:?} +" \ + "$(printf '%s\n\b' '')Please note that this is not the" \ + 'expected way to run the arkenfox user.js updater script.' \ + 'Dot sourcing support is experimental' \ + 'and all function and variable names' \ + 'are still subject to change.' + # Make arkenfox_updater_update_self a no-op as this function + # can not be run reliably when dot sourced. + eval 'arkenfox_updater_update_self() { :; }' && + # Restore the starting aliases. + eval "${_STARTING_ALIASES?}" + fi + else + # Restore the starting aliases. + eval "${_STARTING_ALIASES?}" && + (exit "$status_") + fi