How to Disable Sitejet Globally Without Breaking Existing Websites

Taking Back Control: Granular Sitejet Management on cPanel Servers

The hosting landscape is constantly evolving. Recently, WebPros has heavily integrated Sitejet into the core cPanel experience. While Sitejet is a powerful builder, it’s not the right fit for every hosting provider. Many companies have already invested in competing site builders, prefer to keep their interface lightweight, or are looking forward to WebPros’ own upcoming “Nova” builder and don’t want to invest in Sitejet in the interim.

The challenge for sysadmins is that simply disabling Sitejet globally via the WHM Feature Manager does not affect already deployed websites, but it does prevent customers who are still developing and continuously editing their published Sitejet sites from using the editor. To avoid back-and-forth support tickets to re-enable the editor for those users, we need a way to opt out new users while keeping active users’ editors functional.

Today I’m sharing a script I designed to address this scenario by automating the complex tasks of segmenting users, cloning packages, and reassigning features—so you can manage your site builder offering on your own terms.

To disable Sitejet safely, you must perform a multi-step migration:

  1. Identify users actively publishing with Sitejet.
  2. Backup existing configurations.
  3. Create a new Feature List with Sitejet enabled.
  4. Clone existing hosting Packages and tie them to this new Feature List.
  5. Move the active Sitejet users to these new packages.
  6. Finally, disable Sitejet in the default feature list for everyone else.

Doing this manually via the WHM GUI for hundreds or thousands of accounts is practically impossible and highly prone to error.

Introducing the cPanel Sitejet Management Toolkit I have created two scripts to automate this lifecycle, now available on GitHub.

1. detects_users.sh (The Audit Tool) This script is non-invasive. It scans all accounts (or filters by OWNER=root) and generates a detailed CSV classification of Sitejet usage. It determines if a site is:

  • sitejet_standard_active: Published, deployed, and currently visible.
  • sitejet_present_overridden: Metadata exists, but the user has manually uploaded an index.php, effectively hiding the Sitejet site.
  • sitejet_meta_only: The user opened the builder but never published.

This is your first step to understand your actual exposure before making changes.

2. operate.sh (The Migration Tool) This is an interactive script that performs the heavy lifting. It can operate in two modes: scanning all users with Sitejet data, or using the --active-only flag (recommended) to target strictly active, non-overridden sites.

It handles the entire workflow:

  • Creates timestamped backups of /var/cpanel/packages and /var/cpanel/features.
  • Creates a default_sitejet feature list based on your current default, with Sitejet enabled.
  • Identifies the packages used by target users and clones them (e.g., Starter becomes Starter_with-sitejet).
  • Crucially: It replicates CloudLinux LVE limits to the new cloned packages if present.
  • Moves eligible users to the new _with-sitejet packages via whmapi1 changepackage.
  • Provides the option to disable Sitejet in the standard default feature list.

The Result: An “Opt-In” Workflow After running these scripts, Sitejet is disabled for new signups and existing users on standard packages. If a client demands Sitejet, you simply move them to one of the _with-sitejet packages, and it appears in their cPanel interface. Active users never see a service interruption.

Conclusion Whether you are waiting for Nova, promoting a different builder, or just disagree with the default WebPros orientation, these scripts give you the logic needed to assert control over your cPanel environment without risking existing customer data.

readme.md

# cPanel Sitejet Migration & Management Tools## OverviewAs WebPros pushes Sitejet deeper into the cPanel ecosystem, many hosting providers require granular control over its availability. Some providers prefer not to invest in Sitejet due to existing partnerships with other site builders, or because they are awaiting WebPros' upcoming "Nova" builder and wish to disable Sitejet in the interim.Simply disabling Sitejet globally can break existing customer sites.This repository provides robust, defensive Bash tools to automate the migration of active Sitejet users to a dedicated, parallel packaging structure. This allows administrators to disable Sitejet in the `default` WHM feature list while ensuring uninterrupted service for clients actively using the builder.## Features* **`detects_users.sh`**: Audits the server and generates a CSV classifying usage states (active, overridden, metadata only). Supports filtering by `OWNER=root`.* **Parallel Packaging Model**: Leaves standard packages untouched. Sitejet becomes an "opt-in" by assigning users to parallel packages (e.g., `packageName` -> `packageName_with-sitejet`).* **CloudLinux Integration**: Automates the cloning of LVE limits from original packages to cloned packages using `lvectl` or `lve-manager`.* **Defensive Design**:    * Requires root privileges.    * Syntactically strict Bash (errexit, nounset, pipefail).    * Interactive operation with explicit prompts.    * Full Dry-Run support (`SITEJET_DRY=1`).    * Automated backups of package and feature directories before modification.## Prerequisites* A cPanel & WHM server.* Root access.* `jq` installed (standard in recent cPanel installs, otherwise `dnf install jq`).## Installation```bashmkdir -p /root/workspace/sitejetcd /root/workspace/sitejet# Copy the scripts operate.sh and detects_users.sh into this directorychmod +x operate.sh detects_users.sh```All logs, backups, and artifacts are stored within /root/workspace/sitejet/.Usage1. Audit Current Usage (detects_users.sh)Generate a full report of all accounts utilizing Sitejet, classified by their actual publication status.Bashsudo ./detects_users.shTo limit the audit strictly to accounts owned by root:Bashsudo SITEJET_OWNER_FILTER=root ./detects_users.shArtifact: /root/workspace/sitejet/logs/sitejet_pkg_verify_full.csv2. Execute Migration (operate.sh)This script is interactive. It is highly recommended to run a dry-run first.Dry-Run (Simulate changes):Bashsudo SITEJET_DRY=1 ./operate.sh --active-onlyLive Execution (Recommended mode: Active Users Only):The --active-only flag ensures only users with published, standard (not overridden) Sitejet sites are migrated to the parallel packages.Bashsudo ./operate.sh --active-onlyThe Migration Lifecycle ExplainedWhen running operate.sh, the following workflow is executed based on your prompt responses:    Backup: /var/cpanel/packages and /var/cpanel/features are backed up to /root/workspace/sitejet/backups/[TIMESTAMP].    Feature List Creation: A new feature list named default_sitejet is created as a clone of default, with the Sitejet key enabled.    Package Cloning: Packages used by active Sitejet users are cloned. New package name format: [OriginalName]_with-sitejet.    LVE Replication: If CloudLinux is present, LVE limits from original packages are replicated to the _with-sitejet packages.    Disable Default: An option is provided to set the Sitejet feature key to 0 in the standard /var/cpanel/features/default file.    User Migration: Target users are moved to the new _with-sitejet packages via whmapi1 changepackage.## RollbackThe scripts provide detailed rollback hints upon completion.    To rollback feature/package configurations:    Replace the contents of /var/cpanel/packages/ and /var/cpanel/features/ with the contents of the latest backup directory created by the script.    To rollback users:    Consult /root/workspace/sitejet/logs/sitejet_pkg_moves.log for the "before and after" state, and use whmapi1 changepackage to return them to their original package.DisclaimerThese scripts modify core cPanel configurations and user packages. While designed defensively, they are provided "as-is". The user assumes all responsibility for execution. Always review dry-run output and ensure backups are functional before running live on a production environment.---### Part 3: Sanitized Execution ExampleHere is the example execution, sanitized as requested. Usernames, specific package names (replaced with generic 'Bronze/Silver'), IDs, and timestamps have been changed.```text# /root/workspace/sitejet/operate.sh --active-onlySitejet migration final script (OWNER=root only)Workdir: /root/workspace/sitejetBackups dir: /root/workspace/sitejet/backupsLogs: /root/workspace/sitejet/logsDRY_RUN: 0NOTICE: Running in ACTIVE_ONLY mode. Only users meeting strict publishing criteria will be included.Generate users CSV (only OWNER=root) at /root/workspace/sitejet/sitejet_users.csv now? [y/n] (default: y): y--- DEBUG: Starting generate_users_csv User Scan (find | while read) ------ FILTER: ENFORCING STRICT ACTIVE USER CRITERIA (Published, Deployed, No index.php override) ---...DEBUG: Checking file: /var/cpanel/users/alphaDEBUG: Checking file: /var/cpanel/users/betauserDEBUG: Checking file: /var/cpanel/users/gammasiteDEBUG: Checking file: /var/cpanel/users/deltacorp--- DEBUG: Finishing generate_users_csv (Final Count: 28) ---Detected 28 OWNER=root users with Sitejet metadata.Run detailed detection and produce /root/workspace/sitejet/logs/sitejet_detection_details.csv? [y/n] (default: y): yDetection CSV: /root/workspace/sitejet/logs/sitejet_detection_details.csvCreate full backup of /var/cpanel/packages and /var/cpanel/features to workspace backups now? [y/n] (default: y): yBackups created at: /root/workspace/sitejet/backups/20261015T090001Detected feature key: sitejetCreate /var/cpanel/features/default_sitejet (Sitejet enabled) based on default? [y/n] (default: y): ySitejet-enabled feature list created: /var/cpanel/features/default_sitejet<not present>Disable Sitejet in default feature file (set sitejet=0) now? This is reversible via backup. [y/n] (default: n): yDefault feature sitejet set to 0. Backup: /root/workspace/sitejet/backups/20261015T090001/features-default.bakPackages to clone (3): - Bronze Plan - Silver Plan - Corporate DedicatedProceed to clone packages and bind them to the /var/cpanel/features/default_sitejet feature list? [y/n] (default: y): yProcessing package: Bronze PlanProcessing package: Silver PlanProcessing package: Corporate DedicatedPreview of users CSV (first 20 lines):     1 user,pkg...    17 user1example,Bronze Plan    18 user2test,Silver Plan    19 user3hosting,Bronze Plan    20 user4site,Corporate DedicatedProceed with changepackage for all OWNER=root users in /root/workspace/sitejet/sitejet_users.csv? (Dry-run will simulate) [y/n] (default: n): yRun strict verification across all OWNER=root users and append to /root/workspace/sitejet/logs/sitejet_pkg_verify.csv? [y/n] (default: y): yVerification results appended to /root/workspace/sitejet/logs/sitejet_pkg_verify.csvMigration completed. Artifacts and logs: - Workspace: /root/workspace/sitejet - Backups: /root/workspace/sitejet/backups/20261015T090001 - Actions log: /root/workspace/sitejet/logs/sitejet_actions.log - Moves log: /root/workspace/sitejet/logs/sitejet_pkg_moves.log - Detection details: /root/workspace/sitejet/logs/sitejet_detection_details.csv - Verification CSV: /root/workspace/sitejet/logs/sitejet_pkg_verify.csvRollback hints: - Review /root/workspace/sitejet/logs/sitejet_actions.log and /root/workspace/sitejet/logs/sitejet_pkg_moves.log for MOVED entries. - To rollback a user:     whmapi1 changepackage user=<user> pkg=<original_pkg> - To restore feature files from backup:     cp -a <BACKUP_DIR>/features-default.bak /var/cpanel/features/default     # If default_sitejet existed before the run, restore it too     [ -f "<BACKUP_DIR>/features-default_sitejet.bak" ] && cp -a <BACKUP_DIR>/features-default_sitejet.bak /var/cpanel/features/default_sitejet - To restore all backed-up files:     rm -rf /var/cpanel/features /var/cpanel/packages     cp -a <BACKUP_DIR>/packages_features.<ts>.tgz /var

/root/workspace/sitejet/operate.sh

#!/usr/bin/env bashset -o errexitset -o nounsetset -o pipefail# operate.sh# Interactive Sitejet migration script restricted to accounts owned by OWNER=root## Usage:#   SITEJET_DRY=1 sudo /root/workspace/sitejet/operate.sh               # dry-run preview (broad scope)#   sudo /root/workspace/sitejet/operate.sh --active-only               # interactive commit (strict scope)#   sudo /root/workspace/sitejet/operate.sh --gen-csv                   # generate CSV only (broad scope)## Requirements: run as root; jq and whmapi1 must be available.# ---- Configuration ----FEATURE_BASE="/var/cpanel/features/default"# New configuration for the Sitejet-enabled feature listSITEJET_FEATURE_BASE="/var/cpanel/features/default_sitejet" PACKAGES_DIR="/var/cpanel/packages"WORKDIR="/root/workspace/sitejet"BACKUPS_DIR="${WORKDIR}/backups"LOG_DIR="${WORKDIR}/logs"USERS_CSV="${WORKDIR}/sitejet_users.csv"DETECTION_CSV="${LOG_DIR}/sitejet_detection_details.csv"VERIFICATION_CSV="${LOG_DIR}/sitejet_pkg_verify.csv"ACTIONS_LOG="${LOG_DIR}/sitejet_actions.log"DETAILED_DETECT_LOG="${LOG_DIR}/sitejet_detection.log"DRY_RUN="${SITEJET_DRY:-0}"TMPDIR="${WORKDIR}/tmp_$$"# NEW: Flag to enforce strict active user criteria (1 = enforced, 0 = broad scan)ACTIVE_ONLY=0 # ---- Helpers ----timestamp(){ date -Iseconds; }die(){ echo "$*" >&2; exit 1; }ensure_root(){ [ "$(id -u)" -eq 0 ] || die "Run as root"; }# Ensure dirsprepare_dirs(){  mkdir -p "$WORKDIR" "$BACKUPS_DIR" "$LOG_DIR" "$TMPDIR"  touch "$ACTIONS_LOG" "$DETAILED_DETECT_LOG"  if ! grep -q '^user,' "$VERIFICATION_CSV" 2>/dev/null; then    echo "user,domain,status,index.html,index.php" > "$VERIFICATION_CSV"  fi}log_action(){  local action="$1" target="$2" result="$3" backup="$4"  echo "$(timestamp) | ${action} | ${target} | ${result} | ${backup:-}" >> "$ACTIONS_LOG"}prompt_yesno(){  local prompt="${1:-Continue?}" default="${2:-y}" answer  while :; do    read -r -p "$prompt [y/n] (default: $default): " answer    answer="${answer:-$default}"    case "${answer,,}" in      y|yes) return 0 ;;      n|no) return 1 ;;      *) echo "Please answer y or n." ;;    esac  done}# ---- Prereqs ----check_prereqs(){  command -v jq >/dev/null 2>&1 || die "jq not found. Install jq and retry."  command -v whmapi1 >/dev/null 2>&1 || die "whmapi1 not found in PATH. Ensure whmapi1 available."  prepare_dirs}# ---- Backup utilities ----create_timestamped_backup(){  local ts backup_dir  ts="$(date +%Y%m%dT%H%M%S)"  backup_dir="${BACKUPS_DIR}/${ts}"  mkdir -p "$backup_dir"  if [ "$DRY_RUN" -eq 1 ]; then    echo "DRY_RUN: would create backup at ${backup_dir}"    echo "$backup_dir"    return 0  fi  tar -C /var -czf "${backup_dir}/packages_features.${ts}.tgz" cpanel/packages cpanel/features || true  cp -a "$FEATURE_BASE" "${backup_dir}/features-default.bak" || true  # Backup the new feature base if it exists  [ -f "$SITEJET_FEATURE_BASE" ] && cp -a "$SITEJET_FEATURE_BASE" "${backup_dir}/features-default_sitejet.bak" || true  echo "${backup_dir}"}# ---- Detection & users CSV generation (only OWNER=root) ----# OWNER=root check: read OWNER= value from /var/cpanel/users/<user>is_owner_root(){  # Defensive initialization for nounset  local user=""  user="${1:-}"  local ufile="/var/cpanel/users/${user}"  if [ ! -f "$ufile" ]; then    return 1  fi  # Extract OWNER value if present  local owner  owner="$(awk -F= '/^OWNER=/{print $2; exit}' "$ufile" 2>/dev/null || echo)"  # Treat missing OWNER as root-owned  if [ -z "$owner" ] || [ "$owner" = "root" ]; then    return 0  fi  return 1}	# NEW: Checks if a domain for a user meets the strict "active" criteria# CRITERIA: Published (pub_status=1, date>0), Deployed (has webcard in files list), #           and NOT Overridden (no index.php in webcard folder)check_active_user(){  local user="$1" domain="$2"  local meta="/home/${user}/.cpanel/sitejet/${domain}"    if [ ! -f "$meta" ]; then    return 1 # Metadata missing  fi  local dr_pub dr_lp files_list document_root webcard_dir    # CRITERIA 1: Published Status   dr_pub="$(jq -r '.publish_status // empty' "$meta" 2>/dev/null || echo)"  dr_lp="$(jq -r '.latest_publish_date // 0' "$meta" 2>/dev/null || echo 0)"    if [ "$dr_pub" != "1" ] || [ "${dr_lp:-0}" -eq 0 ]; then    return 1 # Not published  fi  # CRITERIA 2: Active Deployment   files_list="${meta}-files"  if [ ! -f "$files_list" ] || ! grep -q 'webcard' "$files_list"; then    return 1 # No webcard deployment found  fi    # CRITERIA 3: Not Overridden   document_root="$(jq -r '.document_root // empty' "$meta" 2>/dev/null || echo)"  document_root="${document_root:-/home/${user}/public_html}"  document_root="${document_root%/}"  local webcard_dir="${document_root}/webcard"    if [ -d "$webcard_dir" ] && [ -f "${webcard_dir}/index.php" ]; then    return 1 # index.php override detected (not standard active site)  fi    # All strict criteria met: standard active Sitejet user.  return 0}generate_users_csv(){  mkdir -p "$(dirname "$USERS_CSV")"  : > "$USERS_CSV"    # Ensure the CSV starts with a header for clean counting  echo "user,pkg" > "$USERS_CSV"     echo "--- DEBUG: Starting generate_users_csv User Scan (find | while read) ---" >&2  # Output the criteria being applied  if [ "$ACTIVE_ONLY" -eq 1 ]; then    echo "--- FILTER: ENFORCING STRICT ACTIVE USER CRITERIA (Published, Deployed, No index.php override) ---" >&2  fi    # FIX: Use find -print0 | while read -d $'\0' for safe iteration  find /var/cpanel/users/ -maxdepth 1 -type f -print0 | while IFS= read -r -d $'\0' ufile; do        # CRITICAL DEBUG STEP: This is now inside the robust loop structure    echo "DEBUG: Checking file: $ufile" >&2        user="$(basename "$ufile")"    local sjdir="/home/${user}/.cpanel/sitejet"    local has_eligible_domain="false"    # 1. Check if owned by root    if ! is_owner_root "$user"; then        echo "$(timestamp) SKIP_NON_ROOT_OWNER ${user}" >> "$DETAILED_DETECT_LOG"        continue    fi    # 2. Check for Sitejet metadata    if [ -d "$sjdir" ]; then            # Iterate over all domains/metadata files for this user      for meta in "$sjdir"/*; do        [ -f "$meta" ] || continue        [[ "$meta" =~ -files$ ]] && continue                local domain="$(basename "$meta")"        # FILTERING LOGIC        if [ "$ACTIVE_ONLY" -eq 1 ]; then          if check_active_user "$user" "$domain"; then            has_eligible_domain="true"            break # Found one active domain, this user is eligible          fi        else          # If not ACTIVE_ONLY, any Sitejet metadata makes them eligible          has_eligible_domain="true"          break        fi      done            # If user passed filtering (or no filtering was applied), add to CSV      if [ "$has_eligible_domain" = "true" ]; then        # Defensive read for package        pkg="$( { awk -F= '/^PLAN/ {print $2; exit}' "$ufile" 2>/dev/null || echo ""; } )"        pkg="${pkg:-unknown}"                echo "${user},${pkg}" >> "$USERS_CSV"        echo "$(timestamp) Detected Sitejet metadata for user ${user} (pkg=${pkg}, ACTIVE_ONLY=${ACTIVE_ONLY})" >> "$DETAILED_DETECT_LOG"      fi    fi  done    # Since the loop runs in a subshell, we count the lines written to the CSV file  # Note: 'wc -l' includes the header, so we subtract 1.  local final_count  final_count="$(wc -l < "$USERS_CSV")"  final_count=$((final_count > 0 ? final_count - 1 : 0))  echo "$final_count" > "${TMPDIR}/gen_count" # Passes count back to parent shell  log_action "GENERATE_USERS_CSV" "$USERS_CSV" "OK" ""    echo "--- DEBUG: Finishing generate_users_csv (Final Count: $final_count) ---" >&2  return 0}detailed_detection_report(){  : > "${TMPDIR}/detection.tmp"  echo "user,domain,document_root,publish_status,latest_publish_date,files_has_webcard" > "${TMPDIR}/detection.tmp"  while IFS=, read -r user pkg; do    [ -z "$user" ] && continue    # ensure root-owned still (defensive)    if ! is_owner_root "$user"; then      echo "${user},-,-,-,-,0" >> "${TMPDIR}/detection.tmp"      continue    fi    sitejet_dir="/home/${user}/.cpanel/sitejet"    if [ ! -d "$sitejet_dir" ]; then      echo "${user},-,-,-,-,0" >> "${TMPDIR}/detection.tmp"      continue    fi    for meta in "$sitejet_dir"/*; do      [ -f "$meta" ] || continue      [[ "$meta" =~ -files$ ]] && continue      domain="$(basename "$meta")"      dr="$(jq -r '.document_root // empty' "$meta" 2>/dev/null || echo)"      pub="$(jq -r '.publish_status // empty' "$meta" 2>/dev/null || echo)"      lp="$(jq -r '.latest_publish_date // 0' "$meta" 2>/dev/null || echo 0)"      files_list="${meta}-files"      fhw=0      if [ -f "$files_list" ] && grep -q 'webcard' "$files_list"; then        fhw=1      fi      echo "${user},${domain},${dr},${pub},${lp},${fhw}" >> "${TMPDIR}/detection.tmp"    done  done < "$USERS_CSV"  cp "${TMPDIR}/detection.tmp" "$DETECTION_CSV"  log_action "DETECTION_REPORT" "$DETECTION_CSV" "OK" ""}# ---- Feature key detection and feature list management ----detect_feature_key(){  if [ -f "$FEATURE_BASE" ]; then    if grep -qi '^cpanel_sitejet_website_builder=' "$FEATURE_BASE"; then      echo "cpanel_sitejet_website_builder"      return 0    fi    if grep -qi '^sitejet=' "$FEATURE_BASE"; then      echo "sitejet"      return 0    fi  fi  echo "sitejet"}# Creates the sitejet-enabled feature list: default_sitejetcreate_sitejet_feature_base(){  local backup_dir="$1" feature_key="$2"  if [ "$DRY_RUN" -eq 1 ]; then    echo "DRY_RUN: would create ${SITEJET_FEATURE_BASE} from ${FEATURE_BASE} and set ${feature_key}=1"    log_action "CREATE_SITEJET_FEATURE_BASE" "$SITEJET_FEATURE_BASE" "DRY_RUN" "$backup_dir"    return 0  fi    # Copy default feature file  cp -a "$FEATURE_BASE" "$SITEJET_FEATURE_BASE"    # Set the feature key to 1  if grep -qi "^${feature_key}=" "$SITEJET_FEATURE_BASE"; then    sed -i "s/^${feature_key}=.*/${feature_key}=1/I" "$SITEJET_FEATURE_BASE"  else    echo "${feature_key}=1" >> "$SITEJET_FEATURE_BASE"  fi    chown root:root "$SITEJET_FEATURE_BASE"  chmod 644 "$SITEJET_FEATURE_BASE"  log_action "CREATE_SITEJET_FEATURE_BASE" "$SITEJET_FEATURE_BASE" "OK" "$backup_dir"}# Disables Sitejet in the original default feature filedisable_sitejet_in_default(){  local backup_dir="$1" feature_key="$2"  if [ "$DRY_RUN" -eq 1 ]; then    echo "DRY_RUN: would backup and set ${feature_key}=0 in ${FEATURE_BASE}"    log_action "DISABLE_DEFAULT_FEATURE" "$FEATURE_BASE" "DRY_RUN" "$backup_dir"    return 0  fi    # Backup the original default feature file  cp -a "$FEATURE_BASE" "${backup_dir}/features-default.bak"    # Set the feature key to 0  if grep -qi "^${feature_key}=" "$FEATURE_BASE"; then    sed -i "s/^${feature_key}=.*/${feature_key}=0/I" "$FEATURE_BASE"  else    echo "${feature_key}=0" >> "$FEATURE_BASE"  fi  log_action "DISABLE_DEFAULT_FEATURE" "$FEATURE_BASE" "OK" "$backup_dir"}# ---- Per-package featurelist binding (updated to use SITEJET_FEATURE_BASE) ----bind_package_featurelist_whmapi(){  local newpkg="$1" featurelist_name="$2"  if [ "$DRY_RUN" -eq 1 ]; then    echo "DRY_RUN: whmapi1 editpkg name=${newpkg} featurelist=${featurelist_name}"    log_action "BIND_PACKAGE_FEATURELIST" "${newpkg} to ${featurelist_name}" "DRY_RUN" ""    return 0  fi  # Use the single SITEJET_FEATURE_BASE name  if whmapi1 editpkg name="${newpkg}" featurelist="${featurelist_name}" >/dev/null 2>&1; then    log_action "BIND_PACKAGE_FEATURELIST" "${newpkg} to ${featurelist_name}" "OK" ""  else    log_action "BIND_PACKAGE_FEATURELIST" "${newpkg} to ${featurelist_name}" "FAIL" ""  fi}# ---- Clone package file and optional LVE replication ----clone_package_file(){  # FINAL FIX: Use parameter expansion to safely assign the argument.  local oldpkg="${1:-}"   # The local declaration above should prevent the need for this check,  # but we keep it for logical safety (to skip empty package names).  if [ -z "$oldpkg" ]; then    log_action "CLONE_PACKAGE" "NULL_INPUT" "ERROR" ""    return 1  fi    local newpkg="${oldpkg}_with-sitejet"  local oldfile="${PACKAGES_DIR}/${oldpkg}"  local newfile="${PACKAGES_DIR}/${newpkg}"  if [ ! -f "$oldfile" ]; then    log_action "CLONE_PACKAGE" "${oldpkg}" "MISSING_SOURCE" ""    return 1  fi  if [ -f "$newfile" ]; then    log_action "CLONE_PACKAGE" "${newpkg}" "ALREADY_EXISTS" ""    return 0  fi  if [ "$DRY_RUN" -eq 1 ]; then    echo "DRY_RUN: would cp ${oldfile} ${newfile}"    log_action "CLONE_PACKAGE" "${newpkg}" "DRY_RUN" ""    return 0  fi  cp -a "$oldfile" "$newfile"  log_action "CLONE_PACKAGE" "${newpkg}" "OK" ""  return 0}clone_lve_limits(){  # FIX: Two-step initialization to bypass aggressive 'nounset' behavior.  # 1. Declare 'oldpkg' locally and set to empty string.  local oldpkg=""   # 2. Assign the actual argument value from $1.  oldpkg="${1:-}"  # Now that oldpkg is guaranteed to be set, we can safely use it.  local newpkg="${oldpkg}_with-sitejet"     if [ -z "$oldpkg" ]; then    log_action "LVE_CLONE" "NULL_INPUT" "ERROR" ""    return 1  fi    if command -v lvectl >/dev/null 2>&1; then    if [ "$DRY_RUN" -eq 1 ]; then      log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "DRY_RUN" ""      return 0    fi    if lvectl package-clone --from="${oldpkg}" --to="${newpkg}" >/dev/null 2>&1; then      log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "OK" ""    else      log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "FAIL" ""    fi    return 0  fi  if command -v lve-manager >/dev/null 2>&1; then    if [ "$DRY_RUN" -eq 1 ]; then      log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "DRY_RUN" ""      return 0    fi    if lve-manager clone-package "${oldpkg}" "${newpkg}" >/dev/null 2>&1; then      log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "FAIL" ""    else      log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "FAIL" ""    fi    return 0  fi  log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "NO_TOOL" ""  echo "NOTICE: No CloudLinux tool found; manual LVE cloning required for ${oldpkg} -> ${newpkg}"}# ---- Strict verification for a user (appends to verification CSV) ----verify_user_strict(){  local user="$1"  local domain_field domain status has_index_html has_index_php dr pub lp files_list  domain_field="$(awk -F= '/^DNS/ {print $2; exit}' "/var/cpanel/users/$user" 2>/dev/null || true)"  domain="${domain_field:-unknown}"  status="no_sitejet_dir"; has_index_html=0; has_index_php=0  local sitejet_dir="/home/${user}/.cpanel/sitejet"  [ -d "$sitejet_dir" ] || { echo "${user},${domain},no_sitejet_dir,0,0" >> "$VERIFICATION_CSV"; return; }  for meta in "$sitejet_dir"/*; do    [ -f "$meta" ] || continue    [[ "$meta" =~ -files$ ]] && continue    dr="$(jq -r '.document_root // empty' "$meta" 2>/dev/null || echo)"    pub="$(jq -r '.publish_status // empty' "$meta" 2>/dev/null || echo)"    lp="$(jq -r '.latest_publish_date // 0' "$meta" 2>/dev/null || echo 0)"    files_list="${meta}-files"    if [ "$pub" = "1" ] && [ "${lp:-0}" -gt 0 ] && [ -f "$files_list" ] && grep -q 'webcard' "$files_list"; then      dr="${dr:-/home/${user}/public_html}"      dr="${dr%/}"      local webcard="${dr}/webcard"      if [ -d "$webcard" ]; then        [ -f "${webcard}/index.html" ] && has_index_html=1 || true        [ -f "${webcard}/index.php" ] && has_index_php=1 || true        if [ "$has_index_html" -eq 1 ] && [ "$has_index_php" -eq 0 ]; then          status="sitejet_standard_active"          break        else          status="sitejet_present_overridden"        fi      else        status="sitejet_meta_only"      fi    fi  done  echo "${user},${domain},${status},index.html=${has_index_html},index.php=${has_index_php}" >> "$VERIFICATION_CSV"}# ---- Change package for user (exit codes, structured logging) ----change_package_for_user(){  local user="$1" oldpkg="$2" newpkg="${oldpkg}_with-sitejet"  if ! id "$user" &>/dev/null; then    echo "SKIP_USER_NOT_FOUND ${user}"    return 1  fi  if [ ! -f "${PACKAGES_DIR}/${newpkg}" ]; then    echo "SKIP_TARGET_PKG_MISSING ${user} ${newpkg}"    return 2  fi  local current_plan  current_plan="$(whmapi1 accountsummary user="$user" 2>/dev/null | awk -F': ' '/plan:/{print $2}' || true)"  current_plan="${current_plan:-$(awk -F= '/^PLAN/ {print $2; exit}' /var/cpanel/users/$user 2>/dev/null || true)}"  if [ "$current_plan" = "$newpkg" ]; then    echo "NOOP_ALREADY_ON_TARGET ${user} ${newpkg}"    return 0  fi  if [ "$DRY_RUN" -eq 1 ]; then    echo "DRY_RUN Would move ${user} from ${current_plan} to ${newpkg}"    return 0  fi  if whmapi1 changepackage user="$user" pkg="$newpkg" >/dev/null 2>&1; then    echo "MOVED ${user} ${current_plan} -> ${newpkg}"    return 0  else    echo "FAIL_CHANGEPACKAGE ${user} -> ${newpkg}"    return 3  fi}# ---- Rollback hints ----rollback_instructions(){  cat <<EOFRollback hints: - Review ${ACTIONS_LOG} and ${LOG_DIR}/sitejet_pkg_moves.log for MOVED entries. - To rollback a user:     whmapi1 changepackage user=<user> pkg=<original_pkg> - To restore feature files from backup:     cp -a <BACKUP_DIR>/features-default.bak /var/cpanel/features/default     # If default_sitejet existed before the run, restore it too     [ -f "<BACKUP_DIR>/features-default_sitejet.bak" ] && cp -a <BACKUP_DIR>/features-default_sitejet.bak /var/cpanel/features/default_sitejet - To restore all backed-up files:     rm -rf /var/cpanel/features /var/cpanel/packages     cp -a <BACKUP_DIR>/packages_features.<ts>.tgz /varEOF}# ---- Main flow ----ensure_rootcheck_prereqsechoecho "Sitejet migration final script (OWNER=root only)"echo "Workdir: $WORKDIR"echo "Backups dir: $BACKUPS_DIR"echo "Logs: $LOG_DIR"echo "DRY_RUN: $DRY_RUN"echo# Argument parsing for ACTIVE_ONLYif [ "${1:-}" = "--active-only" ]; then    ACTIVE_ONLY=1    shift # Consume the argument    echo "NOTICE: Running in ACTIVE_ONLY mode. Only users meeting strict publishing criteria will be included."fi# CLI --gen-csv modeif [ "${1:-}" = "--gen-csv" ]; then  OUTFILE="${2:-$USERS_CSV}"  generate_users_csv  echo "Generated users CSV at ${OUTFILE}"  exit 0fi# Step 1: generate users.csv interactivelyif prompt_yesno "Generate users CSV (only OWNER=root) at ${USERS_CSV} now?" "y"; then  generate_users_csvelse  [ -f "$USERS_CSV" ] || die "Users CSV not found; aborting."fi# Read the count from the temporary filegen_count="$(cat ${TMPDIR}/gen_count 2>/dev/null || echo 0)"echo "Detected ${gen_count} OWNER=root users with Sitejet metadata."log_action "GENERATE_USERS_SUMMARY" "$USERS_CSV" "COUNT=${gen_count}" ""# Step 2: detailed detection reportif prompt_yesno "Run detailed detection and produce ${DETECTION_CSV}?" "y"; then  detailed_detection_report  echo "Detection CSV: ${DETECTION_CSV}"fi# Step 3: backupif prompt_yesno "Create full backup of /var/cpanel/packages and /var/cpanel/features to workspace backups now?" "y"; then  BACKUP_PATH="$(create_timestamped_backup)"  echo "Backups created at: ${BACKUP_PATH}"  log_action "CREATE_BACKUP" "${BACKUP_PATH}" "OK" "${BACKUP_PATH}"else  die "Backup is required before proceeding. Aborting."fi# Step 4: feature key detection and feature list setupFEATURE_KEY="$(detect_feature_key)"echo "Detected feature key: ${FEATURE_KEY}"# 4a: Create default_sitejet feature listif prompt_yesno "Create ${SITEJET_FEATURE_BASE} (Sitejet enabled) based on default?" "y"; then  create_sitejet_feature_base "$BACKUP_PATH" "$FEATURE_KEY"  echo "Sitejet-enabled feature list created: ${SITEJET_FEATURE_BASE}"else  die "Creating the sitejet feature list is required. Aborting."fi# 4b: Disable Sitejet in default feature listgrep -n -i "^${FEATURE_KEY}=" "$FEATURE_BASE" 2>/dev/null || echo "<not present>"if prompt_yesno "Disable Sitejet in default feature file (set ${FEATURE_KEY}=0) now? This is reversible via backup." "n"; then  disable_sitejet_in_default "$BACKUP_PATH" "$FEATURE_KEY"  echo "Default feature ${FEATURE_KEY} set to 0. Backup: ${BACKUP_PATH}/features-default.bak"else  echo "Skipping disable of default feature as requested."  log_action "DISABLE_DEFAULT_FEATURE" "$FEATURE_BASE" "SKIP" "$BACKUP_PATH"fi# Step 5: build list of packages to clone (unique)packages_to_clone=()while IFS=, read -r u p; do  [ -z "$u" ] && continue  # ensure owner root for safety (defensive)  if ! is_owner_root "$u"; then    echo "$(timestamp) SKIP_NON_ROOT_OWNER_IN_CSV ${u}" >> "$DETAILED_DETECT_LOG"    continue  fi  packages_to_clone+=("$p")done < "$USERS_CSV"mapfile -t packages_to_clone < <(printf '%s\n' "${packages_to_clone[@]}" | awk '!seen[$0]++' | sed '/^$/d')echoecho "Packages to clone (${#packages_to_clone[@]}):"for p in "${packages_to_clone[@]}"; do echo " - $p"; doneif [ "${#packages_to_clone[@]}" -eq 0 ]; then  die "No packages found to clone. Aborting."fiif ! prompt_yesno "Proceed to clone packages and bind them to the ${SITEJET_FEATURE_BASE} feature list?" "y"; then  echo "Aborting before package operations."  exit 0fi# Step 6: clone packages, bind to default_sitejet, LVE cloneSITEJET_FEATURE_LIST="$(basename "$SITEJET_FEATURE_BASE")"for p in "${packages_to_clone[@]}"; do  # Defensive check for empty package name (from previous fix)  if [ -z "$p" ]; then    echo "Skipping empty package name in loop." >&2    continue  fi  # -------------------------------------------------------  echo  echo "Processing package: $p"  clone_package_file "$p" || echo "Warning: clone_package_file returned non-zero for $p"  newpkg="${p}_with-sitejet"  # Bind the new package to the single SITEJET_FEATURE_LIST  bind_package_featurelist_whmapi "$newpkg" "$SITEJET_FEATURE_LIST"  clone_lve_limits "$p"done# Save artifacts to backup foldercp -a "$USERS_CSV" "${BACKUP_PATH}/sitejet_users.csv" || truecp -a "$DETECTION_CSV" "${BACKUP_PATH}/sitejet_detection_details.csv" || truelog_action "SAVE_ARTIFACTS_TO_BACKUP" "${BACKUP_PATH}" "OK" "${BACKUP_PATH}"# Step 7: preview and confirm user migrationsechoecho "Preview of users CSV (first 20 lines):"nl -ba "$USERS_CSV" | sed -n '1,20p'if ! prompt_yesno "Proceed with changepackage for all OWNER=root users in ${USERS_CSV}? (Dry-run will simulate)" "n"; then  echo "Operator chose not to proceed with user reassignment."  exit 0fi# Step 8: reassign users and log: > "${LOG_DIR}/sitejet_pkg_moves.log"while IFS=, read -r user oldpkg; do  [ -z "$user" ] && continue  # enforce owner=root check again defensively  if ! is_owner_root "$user"; then    echo "$(timestamp) SKIP_NON_ROOT_OWNER ${user}" >> "${LOG_DIR}/sitejet_pkg_moves.log"    log_action "CHANGEPACKAGE" "user=${user}" "SKIP_NON_ROOT_OWNER" "${BACKUP_PATH}"    continue  fi  result="$(change_package_for_user "$user" "$oldpkg" 2>&1 || true)"  rc=$?  echo "$(timestamp) | ${result}" >> "${LOG_DIR}/sitejet_pkg_moves.log"  case "$rc" in    0) log_action "CHANGEPACKAGE" "user=${user}" "OK" "${BACKUP_PATH}" ;;    1) log_action "CHANGEPACKAGE" "user=${user}" "SKIP_USER_NOT_FOUND" "${BACKUP_PATH}" ;;    2) log_action "CHANGEPACKAGE" "user=${user}" "SKIP_TARGET_PKG_MISSING" "${BACKUP_PATH}" ;;    3) log_action "CHANGEPACKAGE" "user=${user}" "FAIL_CHANGEPACKAGE" "${BACKUP_PATH}" ;;    *) log_action "CHANGEPACKAGE" "user=${user}" "UNKNOWN_RC_${rc}" "${BACKUP_PATH}" ;;  esacdone < "$USERS_CSV"# Step 9: strict verification post-migrationif prompt_yesno "Run strict verification across all OWNER=root users and append to ${VERIFICATION_CSV}?" "y"; then  while IFS=, read -r user oldpkg; do    [ -z "$user" ] && continue    if ! is_owner_root "$user"; then      echo "$(timestamp) SKIP_NON_ROOT_OWNER_IN_VERIFY ${user}" >> "$DETAILED_DETECT_LOG"      continue    fi    verify_user_strict "$user"  done < "$USERS_CSV"  log_action "POST_MIGRATION_VERIFICATION" "$VERIFICATION_CSV" "OK" "${BACKUP_PATH}"  echo "Verification results appended to ${VERIFICATION_CSV}"fi# Final summary and rollback hintsechoecho "Migration completed. Artifacts and logs:"echo " - Workspace: $WORKDIR"echo " - Backups: $BACKUP_PATH"echo " - Actions log: $ACTIONS_LOG"echo " - Moves log: ${LOG_DIR}/sitejet_pkg_moves.log"echo " - Detection details: ${DETECTION_CSV}"echo " - Verification CSV: ${VERIFICATION_CSV}"rollback_instructionslog_action "RUN_COMPLETE" "$WORKDIR" "OK" "${BACKUP_PATH}"# Cleanuprm -rf "$TMPDIR"exit 0

/root/workspace/sitejet/detects_users.sh

#!/usr/bin/env bash# detects_users.sh# Strict Sitejet verification across all accounts that have /home/<user>/.cpanel/sitejet# Writes CSV to /root/workspace/sitejet/logs/sitejet_pkg_verify_full.csv# Requires: run as root; jq available in PATH.## Usage:#   sudo /root/workspace/sitejet/detects_users.sh#   sudo SITEJET_OWNER_FILTER=root /root/workspace/sitejet/detects_users.sh   # optional OWNER filter (see below)set -o errexitset -o nounsetset -o pipefailOUT_DIR="/root/workspace/sitejet/logs"OUT_FILE="${OUT_DIR}/sitejet_pkg_verify_full.csv"TMPDIR="/root/workspace/sitejet/tmp_verify_$$"OWNER_FILTER="${SITEJET_OWNER_FILTER:-}"   # optional: if set (e.g., root), only include users whose OWNER is empty or equals this value; if empty include all userstimestamp() { date -Iseconds; }die() { echo "$*" >&2; exit 1; }# Prereqs and environment[ "$(id -u)" -eq 0 ] || die "Run as root"command -v jq >/dev/null 2>&1 || die "jq not found in PATH"mkdir -p "$OUT_DIR" "$TMPDIR": > "$OUT_FILE"echo "user,domain,status,index.html,index.php,document_root,publish_status,latest_publish_date,files_has_webcard" > "$OUT_FILE"# Helper: check owner if OWNER_FILTER setowner_allowed() {  local user="$1"  local ufile="/var/cpanel/users/${user}"  [ -f "$ufile" ] || return 1  if [ -z "$OWNER_FILTER" ]; then    return 0  fi  local owner  owner="$(awk -F= '/^OWNER=/{print $2; exit}' "$ufile" 2>/dev/null || echo)"  # treat missing owner as allowed when filtering for "root"  if [ -z "$owner" ] && [ "$OWNER_FILTER" = "root" ]; then    return 0  fi  if [ "$owner" = "$OWNER_FILTER" ]; then    return 0  fi  return 1}# Main loop: scan usersfor ufile in /var/cpanel/users/*; do  [ -f "$ufile" ] || continue  user="$(basename "$ufile")"  # optional owner filter  if ! owner_allowed "$user"; then    continue  fi  sjdir="/home/${user}/.cpanel/sitejet"  [ -d "$sjdir" ] || continue  for meta in "$sjdir"/*; do    [ -f "$meta" ] || continue    # skip manifest files when parsing JSON (we handle manifest separately)    [[ "$meta" =~ -files$ ]] && continue    domain="$(basename "$meta")"    # extract metadata fields (jq required)    document_root="$(jq -r '.document_root // empty' "$meta" 2>/dev/null || echo)"    publish_status="$(jq -r '.publish_status // empty' "$meta" 2>/dev/null || echo)"    latest_publish_date="$(jq -r '.latest_publish_date // 0' "$meta" 2>/dev/null || echo 0)"    files_list="${meta}-files"    files_has_webcard=0    if [ -f "$files_list" ] && grep -q "webcard" "$files_list"; then      files_has_webcard=1    fi    status="no_sitejet_dir"    idx_html=0    idx_php=0    # strict publish checks    if [ "$publish_status" = "1" ] && [ "${latest_publish_date:-0}" -gt 0 ] && [ "$files_has_webcard" -eq 1 ]; then      # determine document_root fallback      if [ -z "$document_root" ]; then        document_root="/home/${user}/public_html"      fi      document_root="${document_root%/}"      webcard_dir="${document_root}/webcard"      if [ -d "$webcard_dir" ]; then        [ -f "${webcard_dir}/index.html" ] && idx_html=1 || true        [ -f "${webcard_dir}/index.php" ] && idx_php=1 || true        if [ "$idx_html" -eq 1 ] && [ "$idx_php" -eq 0 ]; then          status="sitejet_standard_active"        else          status="sitejet_present_overridden"        fi      else        status="sitejet_meta_only"      fi    else      status="sitejet_meta_only"    fi    # escape commas in document_root for CSV    safe_dr="$(printf '%s' "$document_root" | sed 's/,/\\,/g')"    printf '%s,%s,%s,%d,%d,%s,%s,%s,%d\n' \      "$user" "$domain" "$status" "$idx_html" "$idx_php" "$safe_dr" "$publish_status" "$latest_publish_date" "$files_has_webcard" \      >> "$OUT_FILE"  donedone# Summary counts by statusecho "$(timestamp) Wrote verification CSV: $OUT_FILE"awk -F, 'NR>1{cnt[$3]++}END{for (k in cnt) printf "%-30s %d\n", k, cnt[k]}' "$OUT_FILE" | sort -k2 -n -r# finishrm -rf "$TMPDIR"exit 0

Comments

6 comments

  1. A possible bug when there are no accounts detected:

    Disable Sitejet in default feature file (set sitejet=0) now? This is reversible via backup. [y/n] (default: n): y
    Default feature sitejet set to 0. Backup: /root/workspace/sitejet/backups/20260105T102000/features-default.bak
    /root/workspace/sitejet/operate.sh: line 594: packages_to_clone[@]: unbound variable

    Packages to clone (0):
    /root/workspace/sitejet/operate.sh: line 598: packages_to_clone[@]: unbound variable

    It may also be goo to generate the csv first to have an idea :

    /root/workspace/sitejet/operate.sh –gen-csv /root/workspace/sitejet/audit_results.csv
    cat /root/workspace/sitejet/audit_results.csv

    /root/workspace/sitejet/operate.sh –active-only –gen-csv /root/workspace/sitejet/active_audit.csv
    cat /root/workspace/sitejet/audit_results.csv

  2. TODO (non-dry mode should have default Y) for:

    Disable Sitejet in default feature file (set sitejet=0) now? This is reversible via backup. [y/n] (default: n):
    // indicating that it can also be modified via WHM > features manager
    Proceed with changepackage for all OWNER=root users in /root/workspace/sitejet/sitejet_users.csv? (Dry-run will simulate) [y/n] (default: n):

    1. # Changed 4b to default ‘y’
      if prompt_yesno “Disable Sitejet in default feature file (set ${FEATURE_KEY}=0) now? (Also manageable via WHM Feature Manager)” “y”; then
      # … logic …
      fi

      # Changed Step 7 to default ‘y’
      if ! prompt_yesno “Proceed with changepackage for all OWNER=root users in ${USERS_CSV}?” “y”; then
      # … logic …
      fi

  3. lve is not being updated after cloning, lve are stored in /etc/container/ve.cfg, so to clone it, either copy the values from lvectl and reply them using lvectl

    lvectl package-list | grep -i ‘ProPlan’
    ProPlan 200 20480M 0K 80 250 20480 1024
    ProPlan 100 1024M 0K 20 100 1024 1024

    1. unless lve was set using the lve package extension on whm > editing package (which then is defined at the level of the package file), e.g.:

      QUOTA=5120
      lve_inodes_hard=110000
      lve_inodes_soft=100000
      lve_mysql_cpu=DEFAULT
      lve_mysql_io=DEFAULT
      _PACKAGE_EXTENSIONS=lve
      lve_ncpu=DEFAULT
      lve_cpu=100%
      lve_io=1024
      lve_mem=
      lve_pmem=1G
      lve_nproc=100
      lve_iops=1024
      lve_ep=20

Leave a Reply

Your email address will not be published. Required fields are marked *