diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..ac662fca0 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,88 @@ +name: Test + +on: pull_request + +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest] # TODO Add macos-latest and windows-latest + shell: [bash, dash] # TODO Add zsh, yash... + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set shell + if: ${{ matrix.os == 'ubuntu-latest' }} + run: ln -sf /usr/bin/${{ matrix.shell }} /bin/sh + - name: Check exit status definitions + run: | + . ./updater.sh 2>/dev/null + + while IFS='=' read -r name code; 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 + # "exit [n]: If n is specified, but its value is not between 0 and 255 + # inclusively, the exit status is undefined." + # ―https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_21 + [ "$code" -ge 0 ] && [ "$code" -le 255 ] || { + printf '%s %s\n' 'Undefined exit status in the definition:' \ + "$name=$code." >&2 + exit 70 # Internal software error. + } + done </dev/null || { [ "$?" -eq 2 ] && exit 0; } + - name: Tests setup + run: | + useradd -m nonrootuser + echo 'ALL ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + echo 'shopt -s expand_aliases' >> $HOME/.bash_aliases + echo "alias nonsudo='sudo -u nonrootuser sh -c'" >> $HOME/.bash_aliases + set +e + - name: Check that passing a wrong option returns EX_USAGE + run: | + . $HOME/.bash_aliases + nonsudo "./updater.sh -x 2>/dev/null" || { [ "$?" -eq 2 ] && exit 0; } + - name: Check that --help returns EX_OK and not EX__BASE + if: ${{ false }} # TODO Fix this + run: | + . $HOME/.bash_aliases + nonsudo "./updater.sh -h > /dev/null" + - name: Check that if the profile doesn't have at least d-wx permissions, returns EX_UNAVAILABLE + run: | + . $HOME/.bash_aliases + unxable_temp_dir=$(mktemp -d) + chmod 444 $unxable_temp_dir + nonsudo "./updater.sh -p $unxable_temp_dir > /dev/null 2>&1" || { [ "$?" -ne 69 ] && exit 1; } + unwable_temp_dir=$(mktemp -d) + chmod 111 $unwable_temp_dir + nonsudo "./updater.sh -p $unwable_temp_dir > /dev/null 2>&1" || { [ "$?" -ne 69 ] && exit 1; } + exit 0 + - name: Check that if the profiles.ini doesn't exist, returns EX_NOINPUT + run: | + . $HOME/.bash_aliases + temp_dir=$(mktemp -d) + chmod 777 $temp_dir + nonsudo "./updater.sh -l > /dev/null 2>&1" || { [ "$?" -ne 66 ] && exit 1; } + exit 0 + - name: Check that if the profile requires root privileges, returns EX_CONFIG + run: | + . $HOME/.bash_aliases + temp_dir=$(mktemp -d) + chmod 777 $temp_dir + touch $temp_dir/user.js + mkdir $temp_dir/userjs_test + nonsudo "./updater.sh -p $temp_dir > /dev/null 2>&1" || { [ "$?" -ne 78 ] && exit 1; } + exit 0 + - name: Check that the profile gets updated + if: ${{ false }} # TODO Complete this test + run: | + . $HOME/.bash_aliases + temp_dir=$(mktemp -d) + touch $temp_dir/user.js + mkdir $temp_dir/userjs_test + chown -R nonrootuser:nonrootuser $temp_dir + nonsudo "./updater.sh -p $temp_dir" diff --git a/prefsCleaner.sh b/prefsCleaner.sh index b9739b2c3..1e8af06d4 100755 --- a/prefsCleaner.sh +++ b/prefsCleaner.sh @@ -1,185 +1,672 @@ -#!/usr/bin/env bash +#!/bin/sh + +# prefs.js cleaner for macOS, Linux and other Unix operating systems +# authors: @claustromaniac, @earthlng, @9ao9ai9ar +# version: 3.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: +# $ env PROBE_MISSING=1 ./prefsCleaner.sh -v +# $ ( . ./prefsCleaner.sh && WGET__IMPLEMENTATION=wget arkenfox_prefs_cleaner ) +# $ ( TERM=dumb . ./prefsCleaner.sh && yes | arkenfox_prefs_cleaner 2>./stderr.log ) + +# 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. #### +############################################################################### + +# 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. +_EX_SIGHUP=129 # A command was interrupted by SIGHUP (1). +_EX_SIGINT=130 # A command was interrupted by SIGINT (2). +_EX_SIGQUIT=131 # A command was interrupted by SIGQUIT (3). +_EX_SIGABRT=134 # A command was interrupted by SIGABRT (6). +_EX_SIGKILL=137 # A command was interrupted by SIGKILL (9). +_EX_SIGALRM=142 # A command was interrupted by SIGALRM (14). +_EX_SIGTERM=143 # A command was interrupted by SIGTERM (15). +EOF +} + +is_option_set() { # arg: name + [ "$1" = true ] || { + [ "$1" != false ] && [ "${1:-0}" != 0 ] + } +} -## prefs.js cleaner for Linux/Mac -## author: @claustromaniac -## version: 2.1 +print_error() { # args: [ARGUMENT]... + printf '%s\n' "${_TPUT_AF_RED}ERROR: $*${_TPUT_SGR0}" >&2 +} -## special thanks to @overdodactyl and @earthlng for a few snippets that I stol..*cough* borrowed from the updater.sh +print_info() { # args: [ARGUMENT]... + printf '%b' "$*" >&2 +} -## DON'T GO HIGHER THAN VERSION x.9 !! ( because of ASCII comparison in update_prefsCleaner() ) +print_ok() { # args: [ARGUMENT]... + printf '%s\n' "${_TPUT_AF_GREEN}OK: $*${_TPUT_SGR0}" >&2 +} -readonly CURRDIR=$(pwd) +print_warning() { # args: [ARGUMENT]... + printf '%s\n' "${_TPUT_AF_YELLOW}WARNING: $*${_TPUT_SGR0}" >&2 +} -## 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) +probe_mktemp_() { + missing_mktemp_() { + print_error 'Failed to find mktemp or m4 on your system.' + return "${_EX_CNF:-127}" + } + if command -v mktemp >/dev/null 2>&1; then + MKTEMP__IMPLEMENTATION='mktemp' + elif command -v m4 >/dev/null 2>&1; then + MKTEMP__IMPLEMENTATION='m4' + print_warning 'Unable to find mktemp on your system.' \ + "Substituting m4's mkstemp macro for this missing utility." + else + MKTEMP__IMPLEMENTATION= + is_option_set "$PROBE_MISSING" && missing_mktemp_ + fi + mktemp_() { + case $MKTEMP__IMPLEMENTATION in + 'mktemp') command mktemp ;; + 'm4') + # Copied verbatim from https://unix.stackexchange.com/a/181996. + echo 'mkstemp(template)' | + m4 -D template="${TMPDIR:-/tmp}/baseXXXXXX" + ;; + *) missing_mktemp_ ;; + esac + } +} -## fallback for Macs without coreutils -[ -z "$SCRIPT_FILE" ] && SCRIPT_FILE=${BASH_SOURCE[0]} +probe_realpath_() { + # Copied verbatim from https://stackoverflow.com/a/29835459. + # shellcheck disable=all + rreadlink() (# Execute the function in a *subshell* to localize variables and the effect of `cd`. + + target=$1 fname= targetDir= CDPATH= + + # Try to make the execution environment as predictable as possible: + # All commands below are invoked via `command`, so we must make sure that `command` + # itself is not redefined as an alias or shell function. + # (Note that command is too inconsistent across shells, so we don't use it.) + # `command` is a *builtin* in bash, dash, ksh, zsh, and some platforms do not even have + # an external utility version of it (e.g, Ubuntu). + # `command` bypasses aliases and shell functions and also finds builtins + # in bash, dash, and ksh. In zsh, option POSIX_BUILTINS must be turned on for that + # to happen. + { + \unalias command + \unset -f command + } >/dev/null 2>&1 + [ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on # make zsh find *builtins* with `command` too. + + while :; do # Resolve potential symlinks until the ultimate target is found. + [ -L "$target" ] || [ -e "$target" ] || { + command printf '%s\n' "ERROR: '$target' does not exist." >&2 + return 1 + } + command cd "$(command dirname -- "$target")" # Change to target dir; necessary for correct resolution of target path. + fname=$(command basename -- "$target") # Extract filename. + [ "$fname" = '/' ] && fname='' # !! curiously, `basename /` returns '/' + if [ -L "$fname" ]; then + # Extract [next] target path, which may be defined + # *relative* to the symlink's own directory. + # Note: We parse `ls -l` output to find the symlink target + # which is the only POSIX-compliant, albeit somewhat fragile, way. + target=$(command ls -l "$fname") + target=${target#* -> } + continue # Resolve [next] symlink target. + fi + break # Ultimate target reached. + done + targetDir=$(command pwd -P) # Get canonical dir. path + # Output the ultimate target's canonical path. + # Note that we manually resolve paths ending in /. and /.. to make sure we have a normalized path. + if [ "$fname" = '.' ]; then + command printf '%s\n' "${targetDir%/}" + elif [ "$fname" = '..' ]; then + # Caveat: something like /var/.. will resolve to /private (assuming /var@ -> /private/var), i.e. the '..' is applied + # AFTER canonicalization. + command printf '%s\n' "$(command dirname -- "${targetDir}")" + else + command printf '%s\n' "${targetDir%/}/$fname" + fi + ) + if command realpath -- . >/dev/null 2>&1; then + REALPATH__IMPLEMENTATION='realpath' + elif command readlink -f -- . >/dev/null 2>&1; then + REALPATH__IMPLEMENTATION='readlink' + elif command greadlink -f -- . >/dev/null 2>&1; then + REALPATH__IMPLEMENTATION='greadlink' + else + REALPATH__IMPLEMENTATION='rreadlink' + print_warning 'Unable to find realpath or readlink' \ + 'with support for the -f option on your system.' \ + 'Substituting custom portable realpath implementation' \ + 'for these missing utilities.' + fi + realpath_() { # args: FILE... + if [ "$#" -le 0 ]; then + echo 'realpath_: missing operand' >&2 + return "${_EX_USAGE:-2}" + else + realpath__status="${_EX_OK:-0}" + while [ "$#" -gt 0 ]; do + case $REALPATH__IMPLEMENTATION in + 'realpath') command realpath -- "$1" ;; + 'readlink') command readlink -f -- "$1" ;; + 'greadlink') command greadlink -f -- "$1" ;; + *) + # FIXME: Need to resolve basename target. + [ -e "$1" ] && rreadlink "$1" || { + dirname=$(dirname "$1") && + dirname_=$(rreadlink "$dirname") && + basename=$(basename "$1") && + printf '%s\n' "${dirname_%/}/$basename" + } + ;; + esac + status=$? + [ "$status" -eq "${_EX_OK:-0}" ] || realpath__status="$status" + shift + done + return "$realpath__status" + fi + } +} +probe_terminal() { + if [ -t 2 ] && tput setaf bold sgr0 >/dev/null 2>&1; then + _TPUT_AF_RED=$(tput setaf 1) + _TPUT_AF_BLUE=$(tput setaf 4) + _TPUT_AF_BLUE_BOLD=$(tput bold setaf 4) + _TPUT_AF_GREEN=$(tput setaf 2) + _TPUT_AF_YELLOW=$(tput setaf 3) + _TPUT_AF_CYAN=$(tput setaf 6) + _TPUT_SGR0=$(tput sgr0) + else + _TPUT_AF_RED= + _TPUT_AF_BLUE= + _TPUT_AF_BLUE_BOLD= + _TPUT_AF_GREEN= + _TPUT_AF_YELLOW= + _TPUT_AF_CYAN= + _TPUT_SGR0= + fi +} -AUTOUPDATE=true -QUICKSTART=false +probe_wget_() { + missing_wget_() { + print_error 'Failed to find curl or wget on your system.' + return "${_EX_CNF:-127}" + } + if command -v curl >/dev/null 2>&1; then + WGET__IMPLEMENTATION='curl' + elif command -v wget >/dev/null 2>&1; then + WGET__IMPLEMENTATION='wget' + else + WGET__IMPLEMENTATION= + is_option_set "$PROBE_MISSING" && missing_wget_ + fi + wget_() { # args: FILE URL + case $WGET__IMPLEMENTATION in + 'curl') + http_code=$( + command curl --max-redirs 3 -sfw '%{http_code}' -o "$1" "$2" + ) && + [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ] + ;; + 'wget') command wget --max-redirect 3 -qO "$1" "$2" ;; + *) missing_wget_ ;; + esac + } +} -## 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 +# Copied verbatim from https://unix.stackexchange.com/a/464963. +read1() { # arg: + if [ -t 0 ]; then + # if stdin is a tty device, put it out of icanon, set min and + # time to sane value, but don't otherwise touch other input or + # 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 work around 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 +} -fQuit() { - ## change directory back to the original working directory - cd "${CURRDIR}" - [ "$1" -eq 0 ] && echo -e "\n$2" || echo -e "\n$2" >&2 - exit $1 -} - -fUsage() { - echo -e "\nUsage: $0 [-ds]" - echo -e " -Optional Arguments: - -s Start immediately - -d Don't auto-update prefsCleaner.sh" -} - -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 +_arkenfox_init() { + # 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. + # Note that we have to test in a subshell first so that + # the non-interactive POSIX sh is not aborted by an error in set, + # a special built-in utility: + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_01. + # shellcheck disable=SC3040 # In POSIX sh, set option pipefail is undefined. + (set -o pipefail 2>/dev/null) && set -o pipefail + # Disable the nounset option as yash enables it by default, + # which is both inconvenient and against the POSIX recommendation. + # Use ShellCheck or ${parameter?word} to catch unset variables instead. + # The set -o option form is picked for readability and supported + # if the system supports the User Portability Utilities option. + (set +o nounset >/dev/null 2>&1) && set +o nounset || set +u || return + # To prevent the accidental insertion of SGR commands in the grep output, + # even when not directed at a terminal, and because the --color option + # is neither specified in POSIX nor supported by OpenBSD's grep, + # we explicitly set the following three environment variables: + export GREP_COLORS='mt=:ms=:mc=:sl=:cx=:fn=:ln=:bn=:se=' + export GREP_COLOR='0' # Obsolete. Use on macOS and some Unix operating systems + : # where the provided grep implementations do not support GREP_COLORS. + export GREP_OPTIONS= # Obsolete. Use on systems with GNU grep 2.20 or earlier installed. + export LC_ALL=C + exit_status_definitions >/dev/null || return + while IFS='=' read -r name code; do + # Trim trailing whitespace characters. Needed for zsh and yash. + code=${code%"${code##*[![:space:]]}"} # https://stackoverflow.com/a/3352015 + # "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 + # "exit [n]: If n is specified, but its value is not between 0 and 255 + # inclusively, the exit status is undefined." + # ―https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_21 + [ "$code" -ge 0 ] && [ "$code" -le 255 ] || { + printf '%s %s\n' 'Undefined exit status in the definition:' \ + "$name=$code." >&2 + return 70 # Internal software error. + } + (eval readonly "$name=$code" 2>/dev/null) && + eval readonly "$name=$code" || { + eval [ "\"\$$name\"" = "$code" ] && + continue # $name is already readonly and set to $code. + printf '%s %s\n' "Failed to make the exit status $name readonly." \ + 'Try again in a new shell environment?' >&2 + return 75 # Temp failure. + } + done </dev/null 2>&1 || + arkenfox_is_firefox_profile_symlink_locked "$1"; do + 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_is_firefox_profile_symlink_locked() { # arg: DIRECTORY + if [ "$(uname)" = 'Darwin' ]; then # macOS + symlink_lock="${1%/}/.parentlock" + else + symlink_lock="${1%/}/lock" + fi + [ -L "$symlink_lock" ] && + symlink_lock_target=$(realpath_ "$symlink_lock") || + return + lock_signature=$( + basename "$symlink_lock_target" | + 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: $symlink_lock." + return "${_EX_DATAERR:?}" + } + [ "$lock_acquired_ip" != '127.0.0.1' ] || + kill -s 0 "$lock_acquired_pid" 2>/dev/null +} + +arkenfox_script_version() { # arg: {updater.sh|prefsCleaner.sh} + # Why are we not using character classes or range expressions? + # Because they are locale-dependent: https://unix.stackexchange.com/a/654391. + version_format='[0123456789]\{1,\}\.[0123456789]\{1,\}' + version=$( + sed -n "5s/.*version:[[:blank:]]*\($version_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:?}" + } +} + +download_file() { # arg: URL + # The try-finally construct can be implemented as a series 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. + output_temp=$(mktemp_) && + wget_ "$output_temp" "$1" 2>/dev/null && + printf '%s\n' "$output_temp" || { + print_error "Failed to download file from the URL: $1." + return "${_EX_UNAVAILABLE:?}" + } +} + +############################################################################### +#### === prefsCleaner.sh specific functions === #### +############################################################################### + +_arkenfox_prefs_cleaner_init() { + probe_terminal && + PROBE_MISSING=0 probe_wget_ && + PROBE_MISSING=0 probe_mktemp_ && + 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. + [ -z "$ARKENFOX_PREFS_CLEANER_NAME" ] && + ARKENFOX_PREFS_CLEANER_NAME='prefsCleaner.sh' + run_path=$(realpath_ "$0") && + run_dir=$(dirname "$run_path") && + run_name=$(basename "$run_path") || { + print_error 'Failed to resolve the run file path.' + return "${_EX_UNAVAILABLE:?}" + } + ( + readonly "_ARKENFOX_PREFS_CLEANER_RUN_PATH=$run_path" \ + "_ARKENFOX_PREFS_CLEANER_RUN_DIR=$run_dir" \ + "_ARKENFOX_PREFS_CLEANER_RUN_NAME=$run_name" 2>/dev/null + ) && + readonly "_ARKENFOX_PREFS_CLEANER_RUN_PATH=$run_path" \ + "_ARKENFOX_PREFS_CLEANER_RUN_DIR=$run_dir" \ + "_ARKENFOX_PREFS_CLEANER_RUN_NAME=$run_name" || { + [ "$_ARKENFOX_PREFS_CLEANER_RUN_PATH" = "$run_path" ] && + [ "$_ARKENFOX_PREFS_CLEANER_RUN_DIR" = "$run_dir" ] && + [ "$_ARKENFOX_PREFS_CLEANER_RUN_NAME" = "$run_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: [options] + arkenfox_prefs_cleaner_parse_options "$@" && + arkenfox_prefs_cleaner_set_profile_path && + arkenfox_prefs_cleaner_check_nonroot || return + is_option_set "$_ARKENFOX_PREFS_CLEANER_OPTION_D_DONT_UPDATE" || + arkenfox_prefs_cleaner_update_self "$@" || return + arkenfox_prefs_cleaner_banner + if is_option_set "$_ARKENFOX_PREFS_CLEANER_OPTION_S_START"; then + arkenfox_prefs_cleaner_start || return + 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 +} + +arkenfox_prefs_cleaner_usage() { + cat >&2 <&2 <<'EOF' + + + + ╔══════════════════════════╗ + ║ prefs.js cleaner ║ + ║ by claustromaniac ║ + ║ v2.2 ║ + ╚══════════════════════════╝ + +This script should be run from your Firefox profile directory. + +It will remove any 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_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 +} + +arkenfox_prefs_cleaner_start() { + [ -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_check_firefox_profile_lock "${_ARKENFOX_PROFILE_PATH:?}" + backup_dir="${_ARKENFOX_PROFILE_PREFSJS_BACKUP_DIR:?}" + prefsjs_backup="$backup_dir/prefs.js.backup.$(date +"%Y-%m-%d_%H%M")" + mkdir -p "$backup_dir" && + mv -f "${_ARKENFOX_PROFILE_PREFSJS:?}" "$prefsjs_backup" || { + print_error "Failed to backup prefs.js: $prefsjs_backup." + return "${_EX_CANTCREAT:?}" + } + print_ok "Your prefs.js has been backed up: $prefsjs_backup." + print_info 'Cleaning prefs.js...\n\n' + arkenfox_prefs_cleaner_clean "$prefsjs_backup" || return + print_ok 'All done!' +} + +# FIXME: Rewrite. +arkenfox_prefs_cleaner_clean() { # arg: prefs.js + prefs_regex="user_pref[[:blank:]]*\([[:blank:]]*[\"']([^\"']+)[\"'][[:blank:]]*," + all_userjs_prefs=$( + grep -E "$prefs_regex" "${_ARKENFOX_PROFILE_USERJS:?}" | + awk -F"[\"']" '{ print "\"" $2 "\"" }' | + sort | + uniq + ) && + unneeded_prefs=$( + printf '%s\n' "$all_userjs_prefs" | + grep -E -f - "$1" | + grep -E -e "^$prefs_regex" + ) && + printf '%s\n' "$unneeded_prefs" | + grep -v -f - "$1" >"${_ARKENFOX_PROFILE_PREFSJS:?}" +} + +_arkenfox_init && _arkenfox_prefs_cleaner_init +init_status=$? +if [ "$init_status" -eq 0 ]; then + if [ "$_ARKENFOX_PREFS_CLEANER_RUN_NAME" = "$ARKENFOX_PREFS_CLEANER_NAME" ]; then + arkenfox_prefs_cleaner "$@" + else + print_ok 'The 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 + + " \ + 'Please note that this is not the expected way' \ + 'to run the prefs.js cleaner script.' \ + 'Dot sourcing support is experimental' \ + 'and all function and variable names are still subject to change.' + fi +else + # '&& true' to avoid exiting the shell if the shell option errexit is set. + (exit "$init_status") && true +fi diff --git a/updater.sh b/updater.sh index 72c77fcb1..2db36f199 100755 --- a/updater.sh +++ b/updater.sh @@ -1,407 +1,1068 @@ -#!/usr/bin/env bash +#!/bin/sh -## arkenfox user.js updater for macOS and Linux +# arkenfox user.js updater for macOS, Linux and other Unix operating systems +# authors: @overdodactyl, @earthlng, @9ao9ai9ar +# version: 5.0 -## version: 4.0 -## Author: Pat Johnson (@overdodactyl) -## Additional contributors: @earthlng, @ema-pe, @claustromaniac, @infinitewarp +# 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_updater() ) +# Example advanced script usage: +# $ env PROBE_MISSING=1 ./updater.sh -v +# $ ( . ./updater.sh && WGET__IMPLEMENTATION=wget arkenfox_updater ) +# $ ( TERM=dumb . ./updater.sh && yes | arkenfox_updater 2>./stderr.log ) -# 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 +# 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 -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 +############################################################################### +#### === 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. #### +############################################################################### + +# 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. +_EX_SIGHUP=129 # A command was interrupted by SIGHUP (1). +_EX_SIGINT=130 # A command was interrupted by SIGINT (2). +_EX_SIGQUIT=131 # A command was interrupted by SIGQUIT (3). +_EX_SIGABRT=134 # A command was interrupted by SIGABRT (6). +_EX_SIGKILL=137 # A command was interrupted by SIGKILL (9). +_EX_SIGALRM=142 # A command was interrupted by SIGALRM (14). +_EX_SIGTERM=143 # A command was interrupted by SIGTERM (15). +EOF +} + +is_option_set() { # arg: name + [ "$1" = true ] || { + [ "$1" != false ] && [ "${1:-0}" != 0 ] + } +} + +print_error() { # args: [ARGUMENT]... + printf '%s\n' "${_TPUT_AF_RED}ERROR: $*${_TPUT_SGR0}" >&2 +} + +print_info() { # args: [ARGUMENT]... + printf '%b' "$*" >&2 +} + +print_ok() { # args: [ARGUMENT]... + printf '%s\n' "${_TPUT_AF_GREEN}OK: $*${_TPUT_SGR0}" >&2 +} + +print_warning() { # args: [ARGUMENT]... + printf '%s\n' "${_TPUT_AF_YELLOW}WARNING: $*${_TPUT_SGR0}" >&2 +} + +print_yN() { # args: [ARGUMENT]... + printf '%s' "${_TPUT_AF_RED}$* [y/N]${_TPUT_SGR0}" >&2 +} + +probe_mktemp_() { + missing_mktemp_() { + print_error 'Failed to find mktemp or m4 on your system.' + return "${_EX_CNF:-127}" + } + if command -v mktemp >/dev/null 2>&1; then + MKTEMP__IMPLEMENTATION='mktemp' + elif command -v m4 >/dev/null 2>&1; then + MKTEMP__IMPLEMENTATION='m4' + print_warning 'Unable to find mktemp on your system.' \ + "Substituting m4's mkstemp macro for this missing utility." + else + MKTEMP__IMPLEMENTATION= + is_option_set "$PROBE_MISSING" && missing_mktemp_ + fi + mktemp_() { + case $MKTEMP__IMPLEMENTATION in + 'mktemp') command mktemp ;; + 'm4') + # Copied verbatim from https://unix.stackexchange.com/a/181996. + echo 'mkstemp(template)' | + m4 -D template="${TMPDIR:-/tmp}/baseXXXXXX" + ;; + *) missing_mktemp_ ;; + esac + } +} + +probe_open_() { + missing_open_() { + print_error 'Failed to find xdg-open or open on your system.' + return "${_EX_CNF:-127}" + } + if command -v xdg-open >/dev/null 2>&1; then + OPEN__IMPLEMENTATION='xdg-open' + elif command -v open >/dev/null 2>&1; then + OPEN__IMPLEMENTATION='open' + else + OPEN__IMPLEMENTATION= + is_option_set "$PROBE_MISSING" && missing_open_ + fi + open_() { # args: FILE... + case $OPEN__IMPLEMENTATION in + 'xdg-open') + if [ "$#" -le 0 ]; then + command xdg-open + else + open__status="${_EX_OK:-0}" + while [ "$#" -gt 0 ]; do + command xdg-open "$1" + status=$? + [ "$status" -eq "${_EX_OK:-0}" ] || + open__status="$status" + shift + done + return "$open__status" + fi + ;; + 'open') command open "$@" ;; + *) missing_open_ ;; + esac + } +} + +probe_realpath_() { + # Copied verbatim from https://stackoverflow.com/a/29835459. + # shellcheck disable=all + rreadlink() (# Execute the function in a *subshell* to localize variables and the effect of `cd`. + + target=$1 fname= targetDir= CDPATH= + + # Try to make the execution environment as predictable as possible: + # All commands below are invoked via `command`, so we must make sure that `command` + # itself is not redefined as an alias or shell function. + # (Note that command is too inconsistent across shells, so we don't use it.) + # `command` is a *builtin* in bash, dash, ksh, zsh, and some platforms do not even have + # an external utility version of it (e.g, Ubuntu). + # `command` bypasses aliases and shell functions and also finds builtins + # in bash, dash, and ksh. In zsh, option POSIX_BUILTINS must be turned on for that + # to happen. + { + \unalias command + \unset -f command + } >/dev/null 2>&1 + [ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on # make zsh find *builtins* with `command` too. + + while :; do # Resolve potential symlinks until the ultimate target is found. + [ -L "$target" ] || [ -e "$target" ] || { + command printf '%s\n' "ERROR: '$target' does not exist." >&2 + return 1 + } + command cd "$(command dirname -- "$target")" # Change to target dir; necessary for correct resolution of target path. + fname=$(command basename -- "$target") # Extract filename. + [ "$fname" = '/' ] && fname='' # !! curiously, `basename /` returns '/' + if [ -L "$fname" ]; then + # Extract [next] target path, which may be defined + # *relative* to the symlink's own directory. + # Note: We parse `ls -l` output to find the symlink target + # which is the only POSIX-compliant, albeit somewhat fragile, way. + target=$(command ls -l "$fname") + target=${target#* -> } + continue # Resolve [next] symlink target. + fi + break # Ultimate target reached. + done + targetDir=$(command pwd -P) # Get canonical dir. path + # Output the ultimate target's canonical path. + # Note that we manually resolve paths ending in /. and /.. to make sure we have a normalized path. + if [ "$fname" = '.' ]; then + command printf '%s\n' "${targetDir%/}" + elif [ "$fname" = '..' ]; then + # Caveat: something like /var/.. will resolve to /private (assuming /var@ -> /private/var), i.e. the '..' is applied + # AFTER canonicalization. + command printf '%s\n' "$(command dirname -- "${targetDir}")" + else + command printf '%s\n' "${targetDir%/}/$fname" + fi + ) + if command realpath -- . >/dev/null 2>&1; then + REALPATH__IMPLEMENTATION='realpath' + elif command readlink -f -- . >/dev/null 2>&1; then + REALPATH__IMPLEMENTATION='readlink' + elif command greadlink -f -- . >/dev/null 2>&1; then + REALPATH__IMPLEMENTATION='greadlink' + else + REALPATH__IMPLEMENTATION='rreadlink' + print_warning 'Unable to find realpath or readlink' \ + 'with support for the -f option on your system.' \ + 'Substituting custom portable realpath implementation' \ + 'for these missing utilities.' + fi + realpath_() { # args: FILE... + if [ "$#" -le 0 ]; then + echo 'realpath_: missing operand' >&2 + return "${_EX_USAGE:-2}" + else + realpath__status="${_EX_OK:-0}" + while [ "$#" -gt 0 ]; do + case $REALPATH__IMPLEMENTATION in + 'realpath') command realpath -- "$1" ;; + 'readlink') command readlink -f -- "$1" ;; + 'greadlink') command greadlink -f -- "$1" ;; + *) + # FIXME: Need to resolve basename target. + [ -e "$1" ] && rreadlink "$1" || { + dirname=$(dirname "$1") && + dirname_=$(rreadlink "$dirname") && + basename=$(basename "$1") && + printf '%s\n' "${dirname_%/}/$basename" + } + ;; + esac + status=$? + [ "$status" -eq "${_EX_OK:-0}" ] || realpath__status="$status" + shift + done + return "$realpath__status" + fi + } +} + +probe_terminal() { + if [ -t 2 ] && tput setaf bold sgr0 >/dev/null 2>&1; then + _TPUT_AF_RED=$(tput setaf 1) + _TPUT_AF_BLUE=$(tput setaf 4) + _TPUT_AF_BLUE_BOLD=$(tput bold setaf 4) + _TPUT_AF_GREEN=$(tput setaf 2) + _TPUT_AF_YELLOW=$(tput setaf 3) + _TPUT_AF_CYAN=$(tput setaf 6) + _TPUT_SGR0=$(tput sgr0) + else + _TPUT_AF_RED= + _TPUT_AF_BLUE= + _TPUT_AF_BLUE_BOLD= + _TPUT_AF_GREEN= + _TPUT_AF_YELLOW= + _TPUT_AF_CYAN= + _TPUT_SGR0= + fi +} + +probe_wget_() { + missing_wget_() { + print_error 'Failed to find curl or wget on your system.' + return "${_EX_CNF:-127}" + } + if command -v curl >/dev/null 2>&1; then + WGET__IMPLEMENTATION='curl' + elif command -v wget >/dev/null 2>&1; then + WGET__IMPLEMENTATION='wget' + else + WGET__IMPLEMENTATION= + is_option_set "$PROBE_MISSING" && missing_wget_ + fi + wget_() { # args: FILE URL + case $WGET__IMPLEMENTATION in + 'curl') + http_code=$( + command curl --max-redirs 3 -sfw '%{http_code}' -o "$1" "$2" + ) && + [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ] + ;; + 'wget') command wget --max-redirect 3 -qO "$1" "$2" ;; + *) missing_wget_ ;; + esac + } +} + +# Copied verbatim from https://unix.stackexchange.com/a/464963. +read1() { # arg: + if [ -t 0 ]; then + # if stdin is a tty device, put it out of icanon, set min and + # time to sane value, but don't otherwise touch other input or + # 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 work around 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_init() { + # 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. + # Note that we have to test in a subshell first so that + # the non-interactive POSIX sh is not aborted by an error in set, + # a special built-in utility: + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_01. + # shellcheck disable=SC3040 # In POSIX sh, set option pipefail is undefined. + (set -o pipefail 2>/dev/null) && set -o pipefail + # Disable the nounset option as yash enables it by default, + # which is both inconvenient and against the POSIX recommendation. + # Use ShellCheck or ${parameter?word} to catch unset variables instead. + # The set -o option form is picked for readability and supported + # if the system supports the User Portability Utilities option. + (set +o nounset >/dev/null 2>&1) && set +o nounset || set +u || return + # To prevent the accidental insertion of SGR commands in the grep output, + # even when not directed at a terminal, and because the --color option + # is neither specified in POSIX nor supported by OpenBSD's grep, + # we explicitly set the following three environment variables: + export GREP_COLORS='mt=:ms=:mc=:sl=:cx=:fn=:ln=:bn=:se=' + export GREP_COLOR='0' # Obsolete. Use on macOS and some Unix operating systems + : # where the provided grep implementations do not support GREP_COLORS. + export GREP_OPTIONS= # Obsolete. Use on systems with GNU grep 2.20 or earlier installed. + export LC_ALL=C + exit_status_definitions >/dev/null || return + while IFS='=' read -r name code; do + # Trim trailing whitespace characters. Needed for zsh and yash. + code=${code%"${code##*[![:space:]]}"} # https://stackoverflow.com/a/3352015 + # "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 + # "exit [n]: If n is specified, but its value is not between 0 and 255 + # inclusively, the exit status is undefined." + # ―https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_21 + [ "$code" -ge 0 ] && [ "$code" -le 255 ] || { + printf '%s %s\n' 'Undefined exit status in the definition:' \ + "$name=$code." >&2 + return 70 # Internal software error. + } + (eval readonly "$name=$code" 2>/dev/null) && + eval readonly "$name=$code" || { + eval [ "\"\$$name\"" = "$code" ] && + continue # $name is already readonly and set to $code. + printf '%s %s\n' "Failed to make the exit status $name readonly." \ + 'Try again in a new shell environment?' >&2 + return 75 # Temp failure. + } + done <&2 </dev/null && + printf '%s\n' "$output_temp" || { + print_error "Failed to download file from the URL: $1." + return "${_EX_UNAVAILABLE:?}" + } +} + +############################################################################### +#### === updater.sh specific functions === #### +############################################################################### + +_arkenfox_updater_init() { + # The variable assignments before a function in a simple command + # are not guaranteed to not persist after the completion of the function: + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_09_01. + # In fact, they do persist in both ksh and zsh, + # so we have to resort to this workaround. + probe_missing=$PROBE_MISSING + probe_terminal && + PROBE_MISSING=1 probe_wget_ && + PROBE_MISSING=1 probe_mktemp_ && + probe_realpath_ && + PROBE_MISSING=$probe_missing probe_open_ || + 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. + [ -z "$ARKENFOX_UPDATER_NAME" ] && ARKENFOX_UPDATER_NAME='updater.sh' + run_path=$(realpath_ "$0") && + run_dir=$(dirname "$run_path") && + run_name=$(basename "$run_path") || { + print_error 'Failed to resolve the run file path.' + return "${_EX_UNAVAILABLE:?}" + } + ( + readonly "_ARKENFOX_UPDATER_RUN_PATH=$run_path" \ + "_ARKENFOX_UPDATER_RUN_DIR=$run_dir" \ + "_ARKENFOX_UPDATER_RUN_NAME=$run_name" 2>/dev/null + ) && + readonly "_ARKENFOX_UPDATER_RUN_PATH=$run_path" \ + "_ARKENFOX_UPDATER_RUN_DIR=$run_dir" \ + "_ARKENFOX_UPDATER_RUN_NAME=$run_name" || { + [ "$_ARKENFOX_UPDATER_RUN_PATH" = "$run_path" ] && + [ "$_ARKENFOX_UPDATER_RUN_DIR" = "$run_dir" ] && + [ "$_ARKENFOX_UPDATER_RUN_NAME" = "$run_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: [options] + arkenfox_updater_parse_options "$@" && + arkenfox_updater_set_profile_path && + arkenfox_updater_check_nonroot || return + arkenfox_updater_exec_general_options + status=$? + # The exit status _EX__BASE indicates that no general option is executed. + # When curl is used, it may also return with exit status _EX__BASE (64) + # if the requested FTP SSL level failed, but that is not applicable here. + [ "$status" -eq "${_EX__BASE:?}" ] || return "$status" + arkenfox_updater_banner + is_option_set "$_ARKENFOX_UPDATER_OPTION_D_DONT_UPDATE" || + arkenfox_updater_update_self "$@" || return + arkenfox_updater_update_userjs || return +} +arkenfox_updater_usage() { + cat >&2 <&2 # Echo usage string to standard error - echo -e " -Optional Arguments: +${_TPUT_AF_BLUE}Usage: $ARKENFOX_UPDATER_NAME [-h|-r]${_TPUT_SGR0} +${_TPUT_AF_BLUE} $ARKENFOX_UPDATER_NAME [UPDATER_OPTION]... [USERJS_OPTION]...${_TPUT_SGR0} + +General options: -h Show this help message and exit. - -p PROFILE Path to your Firefox profile (if different than the dir of this script) - IMPORTANT: If the path contains spaces, wrap the entire argument in quotes. - -l Choose your Firefox profile from a list - -u Update updater.sh and execute silently. Do not seek confirmation. + -r Only download user.js to a temporary file and open it. + +Updater options: -d Do not look for updates to updater.sh. + -u Update updater.sh and execute silently. Do not seek confirmation. + +user.js options: + -p PROFILE Path to your Firefox profile (if different than the dir of this script). + IMPORTANT: If the path contains spaces, wrap the entire argument in quotes. + -l Choose your Firefox profile from a list. -s Silently update user.js. Do not seek confirmation. - -b Only keep one backup of each file. -c Create a diff file comparing old and new user.js within userjs_diffs. - -o OVERRIDE Filename or path to overrides file (if different than user-overrides.js). - If used with -p, paths should be relative to PROFILE or absolute paths + -b Only keep one backup of each file. + -e Activate ESR related preferences. + -n Do not append any overrides, even if user-overrides.js exists. + -o OVERRIDES Filename or path to overrides file (if different than user-overrides.js). + If used with -p, paths should be relative to PROFILE or absolute paths. If given a directory, all files inside will be appended recursively. You can pass multiple files or directories by passing a comma separated list. - Note: If a directory is given, only files inside ending in the extension .js are appended - IMPORTANT: Do not add spaces between files/paths. Ex: -o file1.js,file2.js,dir1 - IMPORTANT: If any file/path contains spaces, wrap the entire argument in quotes. - Ex: -o \"override folder\" - -n Do not append any overrides, even if user-overrides.js exists. + Note: If a directory is given, only files inside ending in the extension .js are appended. + IMPORTANT: Do not add spaces between files/paths. Ex: -o file1.js,file2.js,dir1 + IMPORTANT: If any file/path contains spaces, wrap the entire argument in quotes. Ex: -o "override folder" -v Open the resulting user.js file. - -r Only download user.js to a temporary file and open it. - -e Activate ESR related preferences." - echo - exit 1 -} - -######################### -# File Handling # -######################### - -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 -} - -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 +} + +arkenfox_updater_parse_options() { # args: [options] + OPTIND=1 # OPTIND must be manually reset between multiple calls to getopts. + _OPTIONS_PARSED=0 + # IMPORTANT! Make sure to initialize all options! + _ARKENFOX_UPDATER_OPTION_H_HELP= + _ARKENFOX_UPDATER_OPTION_R_READ_ONLY= + _ARKENFOX_UPDATER_OPTION_D_DONT_UPDATE= + _ARKENFOX_UPDATER_OPTION_U_UPDATER_SILENT= + _ARKENFOX_UPDATER_OPTION_P_PROFILE_PATH= + _ARKENFOX_UPDATER_OPTION_L_LIST_FIREFOX_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= + while getopts 'hrdup:lscbeno:v' opt; do + _OPTIONS_PARSED=$((_OPTIONS_PARSED + 1)) + case $opt in + # General options + h) _ARKENFOX_UPDATER_OPTION_H_HELP=1 ;; + r) _ARKENFOX_UPDATER_OPTION_R_READ_ONLY=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_FIREFOX_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 +} - # 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_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_FIREFOX_PROFILES"; then + _ARKENFOX_PROFILE_PATH=$(arkenfox_select_firefox_profile_path) || return + else + _ARKENFOX_PROFILE_PATH=$_ARKENFOX_UPDATER_RUN_DIR + fi + _ARKENFOX_PROFILE_PATH=$(realpath_ "$_ARKENFOX_PROFILE_PATH") && + [ -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" } -getProfilePath() { - declare -r f1=~/Library/Application\ Support/Firefox/profiles.ini - declare -r f2=~/.mozilla/firefox/profiles.ini +arkenfox_updater_check_nonroot() { + if [ "$(id -u)" -eq 0 ]; then + print_error "You shouldn't run this with elevated privileges" \ + '(such as with doas/sudo).' + return "${_EX_USAGE:?}" + fi + root_owned_files=$( + find "${_ARKENFOX_PROFILE_USERJS:?}" \ + "${_ARKENFOX_PROFILE_USERJS_BACKUP_DIR:?}/" \ + "${_ARKENFOX_PROFILE_USERJS_DIFF_DIR:?}/" \ + -user 0 -print 2>/dev/null + ) + 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 [ "$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 [ "$_OPTIONS_PARSED" -eq 1 ]; then + if is_option_set "$_ARKENFOX_UPDATER_OPTION_H_HELP"; then + arkenfox_updater_usage 2>&1 + return + elif is_option_set "$_ARKENFOX_UPDATER_OPTION_R_READ_ONLY"; then + arkenfox_updater_wget__open__userjs + return + fi else - echo -e "${RED}Error: Sorry, -l is not supported for your OS${NC}" - exit 1 + if is_option_set "$_ARKENFOX_UPDATER_OPTION_H_HELP" || + is_option_set "$_ARKENFOX_UPDATER_OPTION_R_READ_ONLY"; then + arkenfox_updater_usage + return "${_EX_USAGE:?}" + fi fi - #else - # PROFILE_PATH already set by user with -p - fi + return "${_EX__BASE:?}" +} + +arkenfox_updater_wget__open__userjs() { + master_userjs=$( + download_file \ + 'https://raw.githubusercontent.com/arkenfox/user.js/master/user.js' + ) && + master_userjs_js="$master_userjs.js" && + mv "$master_userjs" "$master_userjs_js" && + print_ok "user.js was saved to the temporary file: $master_userjs_js." && + open_ "$master_userjs_js" } -######################### -# Update updater.sh # -######################### +arkenfox_updater_banner() { + cat >&2 <&2 <> 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 + userjs_backup=$(arkenfox_updater_backup_userjs "$userjs") && + mv "$master_userjs" "$userjs" && + print_ok 'user.js has been backed up' \ + 'and replaced with the latest version!' || + return + arkenfox_updater_customize_userjs "$userjs" || return + if is_option_set "$_ARKENFOX_UPDATER_OPTION_C_COMPARE"; then + diff_file=$(arkenfox_updater_diff_userjs "$userjs" "$userjs_backup") + diff_status=$? + if [ -n "$diff_file" ]; then + [ "$diff_status" -eq "${_EX_FAIL:?}" ] || + print_warning "Unexpected diff status: $diff_status." + print_ok "A diff file was created: $diff_file." + else + [ "$diff_status" -eq "${_EX_OK:?}" ] || return "$diff_status" + print_warning 'Your new user.js file appears to be identical.' \ + 'No diff file was created.' + is_option_set "$_ARKENFOX_UPDATER_OPTION_B_BACKUP_SINGLE" || + rm "$userjs_backup" + fi fi - fi - - # Copy a version of user.js to diffs folder for later comparison - if [ "$COMPARE" = true ]; then - mkdir -p userjs_diffs - cp user.js userjs_diffs/past_user.js &>/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}" + is_option_set "$_ARKENFOX_UPDATER_OPTION_V_VIEW" && open_ "$userjs" +} + +arkenfox_updater_backup_userjs() { # arg: user.js + backup_dir="${_ARKENFOX_PROFILE_USERJS_BACKUP_DIR:?}" + if is_option_set "$_ARKENFOX_UPDATER_OPTION_B_BACKUP_SINGLE"; then + userjs_backup="$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 + userjs_backup="$backup_dir/user.js.backup.$(date +"%Y-%m-%d_%H%M")" + fi + # The -p option is used to suppress errors if directory exists. + mkdir -p "$backup_dir" && + cp "$1" "$userjs_backup" && + printf '%s\n' "$userjs_backup" +} + +arkenfox_updater_customize_userjs() { # arg: user.js + if is_option_set "$_ARKENFOX_UPDATER_OPTION_E_ESR"; then + # Why are we not using character classes or range expressions? + # Because they are locale-dependent: https://unix.stackexchange.com/a/654391. + userjs_temp=$(mktemp_) && + sed 's/\/\* \(ESR[0123456789]\{2,\}\.x still uses all.*\)/\/\/ \1/' \ + "$1" >"$userjs_temp" && + mv "$userjs_temp" "$1" && + print_ok 'ESR related preferences have been activated!' || + return + fi + if ! is_option_set "$_ARKENFOX_UPDATER_OPTION_N_NO_OVERRIDES"; then + : "${_ARKENFOX_PROFILE_PATH:?}" + if [ -n "$_ARKENFOX_UPDATER_OPTION_O_OVERRIDES" ]; then + overrides=$_ARKENFOX_UPDATER_OPTION_O_OVERRIDES + else + overrides="${_ARKENFOX_PROFILE_PATH%/}/user-overrides.js" + fi + ( + IFS=, + (set -o noglob 2>/dev/null) && set -o noglob || set -f || return + # shellcheck disable=SC2086 # Double quote to prevent globbing and word splitting. + arkenfox_updater_append_userjs_overrides $overrides + ) 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 +} + +arkenfox_updater_append_userjs_overrides() { # args: FILE... + userjs="${_ARKENFOX_PROFILE_USERJS:?}" + while [ "$#" -gt 0 ]; do + override=$(realpath_ "$1") || return + if [ -f "$override" ]; then + echo >>"$userjs" && + cat -- "$override" >>"$userjs" && + print_ok "Override file appended: $override." || + return + elif [ -d "$override" ]; then + (set +o noglob 2>/dev/null) && set +o noglob || set +f || return + for overridejs in "$override"/*.js; do + arkenfox_updater_append_userjs_overrides "$overridejs" || + return + done + else + print_warning "Could not find override file: $override." + fi + shift done - fi -fi +} -show_banner -update_updater "$@" +arkenfox_updater_diff_userjs() { # args: FILE1 FILE2 + diff_dir="${_ARKENFOX_PROFILE_USERJS_DIFF_DIR:?}" + mkdir -p "$diff_dir" && + new_userjs_stripped=$(mktemp_) && + old_userjs_stripped=$(mktemp_) && + remove_js_comments "$1" >"$new_userjs_stripped" && + remove_js_comments "$2" >"$old_userjs_stripped" || + return "${_EX_UNAVAILABLE:?}" + diff=$(diff -b -U 0 "$old_userjs_stripped" "$new_userjs_stripped") + diff_status=$? + if [ -n "$diff" ]; then + diff_file="$diff_dir/diff_$(date +"%Y-%m-%d_%H%M").txt" + printf '%s\n' "$diff" >"$diff_file" && + printf '%s\n' "$diff_file" || + return "${_EX_UNAVAILABLE:?}" + fi + return "$diff_status" +} -getProfilePath # updates PROFILE_PATH or exits on error -cd "$PROFILE_PATH" || exit 1 +# This should ideally be placed immediately after read1, +# but then it would break the JetBrain IDEs' syntax highlighting +# and the functions outline in the structure tool window, +# so it is defined last to minimize disruption. +remove_js_comments() { # arg: FILE + # Copied verbatim from the public domain sed script at + # https://sed.sourceforge.io/grabbag/scripts/remccoms3.sed. + # The best POSIX solution on the internet, though it does not handle files + # with syntax errors in C as well as emacs does, e.g. + : 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 + # The reference output is given by: + # 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. + remccoms3=$( + cat <<'EOF' +#! /bin/sed -nf -# 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 +# Remove C and C++ comments, by Brian Hiles (brian_hiles@rocketmail.com) -update_userjs +# 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 + } -cd "$CURRDIR" + 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 + ) + # Setting LC_ALL=C in _arkenfox_init helps prevent an indefinite loop: + # https://stackoverflow.com/q/13061785/#comment93013794_13062074. + sed -n "$remccoms3" "$1" | + sed '/^[[:space:]]*$/d' # Remove blank lines. +} + +_arkenfox_init && _arkenfox_updater_init +init_status=$? +if [ "$init_status" -eq 0 ]; then + if [ "$_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.' \ + " + + Detected name of the run file: $_ARKENFOX_UPDATER_RUN_NAME + ARKENFOX_UPDATER_NAME: $ARKENFOX_UPDATER_NAME + + " \ + '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.' + fi +else + # '&& true' to avoid exiting the shell if the shell option errexit is set. + (exit "$init_status") && true +fi