From 481c45397b90a83b52f57e9044e228c53fab19f1 Mon Sep 17 00:00:00 2001 From: 9ao9ai9ar <32834976+9ao9ai9ar@users.noreply.github.com> Date: Fri, 18 Oct 2024 04:00:41 +0800 Subject: [PATCH] Shell script rewrite to comply with POSIX and best practices. The rewrite is focused on the following five areas of interest: 1. Portability. The scripts have been tested to work in recent versions of the following operating systems and shells: macOS, Linux (Fedora, Debian, Ubuntu, openSUSE, Arch, Alpine, NixOS), BSD (FreeBSD, OpenBSD, NetBSD, DragonFly), SunOS (Solaris, OpenIndiana), Haiku; bash, dash, ash, ksh, oksh, zsh, XPG4 sh, pdksh, mksh, yash, posh, gwsh, bosh, osh. 2. Robustness. Employ secure shell scripting techniques, incorporate battle-tested open source code, clear all ShellCheck warnings, and fail early. 3. Composability. Put (almost) everything inside functions and make the scripts dot source friendly. 4. Consistency. Use tput to abstract away terminal color codes, write templated diagnostic messages and follow conventions in the use of exit status and redirections. 5. Readability. Comment extensively, assign descriptive names to variables and functions, and use here-documents to ease reading and writing multi-line messages. Known behavioral changes: 1. There are changes to the way some options are parsed and acted on. For example, when both the -l and -p options are specified, -l will be ignored; in the old behavior, the last specified option would take effect. Also, an old quirk where passing the argument 'list' to -p was equivalent to specifying the -l option has been fixed. 2. The -h, -l and -p options of updater.sh have been added to prefsCleaner.sh as well. 3. All temporary files are now created using mktemp and no longer actively deleted, so users won't find them in the working directory anymore in the case of error. 4. The old prefs.js cleaning logic, which relied on non-POSIX features, is not preserved in the rewrite. Resolves #1855 Resolves #1446 Fixes #1810 --- prefsCleaner.sh | 1290 ++++++++++++++++++++++++++++++----- updater.sh | 1738 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 2476 insertions(+), 552 deletions(-) diff --git a/prefsCleaner.sh b/prefsCleaner.sh index b9739b2c3..b38ce6b83 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 72c77fcb1..846975ce6 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