OwlCyberSecurity - MANAGER
Edit File: modify_featurelist
#!/usr/local/cpanel/3rdparty/bin/perl # Copyright 2025 WebPros International, LLC # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited. package scripts::modify_featurelist; use cPstrict; =encoding utf-8 =head1 NAME modify_featurelist =head1 USAGE modify_featurelist --config <path-to-config> or modify_featurelist --flag <run-once-flag> --feature <feature-name> [ --enable | --disable ] [ --list <feature-list-name> | --all ] [ --verbose ] =head1 DESCRIPTION This script modifies feature lists to enable or disable a certain feature. The script will only run once per server using the provided flag as a lock to decide if the code should run. When you pass C<--config>, provide a JSON with Comments (JSONC) file with the following keys: =over =item flag - string - The run-once flag. =item condition - object - Optional component rules to evaluate. Conditions can only be provided in the config file. When a condition is present, this script only updates feature lists when the condition evaluates true against the local server state. =item all - boolean - Run on all feature lists except C<disabled>. Use either the C<all> attribute or the C<list> attribute. =item list - array - The feature lists to modify. =item feature - string - cPanel feature name. =item enable - boolean - True to enable by default, false to disable. =item verbose - boolean - Optional. Print progress details. =item add_only - boolean - Optional. If true, only adds the feature if it does not already exist in the feature list. This preserves existing user choices. If false or omitted, this script sets the feature value even when the feature already exists. =back B<NOTE:> Command-line parameters override values from the config file. When C<--all> is provided, this script attempts to add the feature to all feature lists except C<disabled>. Feature-list-specific validation, including standalone feature rules, is delegated to C<Cpanel::Features::update_featurelist>. If validation fails for a specific feature list, this script logs a warning, skips that list, and continues processing the rest. When one or more C<--list> arguments are provided, this script updates the specified feature lists. You must provide either C<--config> or the following: C<--flag>, C<--feature>, and either C<--enable> or C<--disable>. All other arguments are optional. =head1 METHODS =cut use Cpanel::ConfigFiles (); use Cpanel::Exception (); use Cpanel::Features (); use Cpanel::Features::Load (); use Cpanel::FileUtils::TouchFile (); use Cpanel::Version (); use Cpanel::Version::Compare (); use Cpanel::Imports; use parent qw( Cpanel::HelpfulScript ); use constant _OPTIONS => ( 'config=s', 'feature=s', 'list=s@', 'all', 'enable', 'disable', 'flag=s', 'verbose!' ); our $VERSION_DIR = "/var/cpanel/version"; use constant PLUGIN_DIR => '/var/cpanel/plugins/'; __PACKAGE__->new(@ARGV)->run() if !caller; sub _verify_directories () { if ( !-e $Cpanel::ConfigFiles::FEATURES_DIR ) { mkdir( $Cpanel::ConfigFiles::FEATURES_DIR, 0755 ) or do { logger()->warn("Unable to create feature directory '$Cpanel::ConfigFiles::FEATURES_DIR': $!"); return; }; } if ( !-e $VERSION_DIR ) { mkdir( $VERSION_DIR, 0755 ) or do { logger()->warn("Unable to create run lock directory '$VERSION_DIR': $!"); return; }; } return 1; } sub get_featurelists() { opendir( my $dh, $Cpanel::ConfigFiles::FEATURES_DIR ) || do { logger()->warn("Cannot open directory: $Cpanel::ConfigFiles::FEATURES_DIR."); return; }; my @feature_lists = grep { $_ !~ /^\.\.?$/ && $_ ne 'disabled' } readdir($dh); closedir($dh) or die "Failed to retrieve the featurelist: $!"; return @feature_lists; } =head2 modify_feature_for_all_feature_lists($feature, $value, $verbose, $add_only) Adds a feature and value to all feature lists. =head3 ARGUMENTS =over =item * C<$feature> - The feature to add. =item * C<$value> - 1 to enable the feature, 0 to disable the feature. =item * C<$verbose> - 1 to print progress details, 0 to run quietly. =item * C<$add_only> - Optional. If true, only adds the feature if it is not already present. =back =cut sub modify_feature_for_all_feature_lists ( $feature, $value, $verbose, $add_only = 0 ) { my @feature_lists = get_featurelists(); my $mail_only_flag = PLUGIN_DIR . "$feature/mail_only"; foreach my $feature_list (@feature_lists) { if ( $feature_list eq 'Mail Only' && $value && !-e $mail_only_flag ) { say "Disabling '$feature' for '$feature_list' featurelist." if $verbose; modify_featurelist( $feature_list, $feature, 0, $verbose, $add_only ); next; } modify_featurelist( $feature_list, $feature, $value, $verbose, $add_only ); } return 1; } =head2 modify_featurelist($feature_list, $feature, $value, $verbose, $add_only) Initialize the feature in the feature list with the provided default value. This function delegates feature validation to C<Cpanel::Features::update_featurelist>. =head3 ARGUMENTS =over =item * C<$feature_list> - The feature list to modify. =item * C<$feature> - The feature to add. =item * C<$value> - 1 to enable the feature, 0 to disable the feature. =item * C<$verbose> - 1 to print progress details, 0 to run quietly. =item * C<$add_only> - Optional. If true, only adds the feature if it is not already present. =back =cut sub modify_featurelist ( $feature_list, $feature, $value, $verbose, $add_only = 0 ) { say "Attempting to modify: $feature_list" if $verbose; my $features = eval { Cpanel::Features::Load::load_featurelist($feature_list) }; if ( my $error = $@ ) { logger()->warn("Unable to add default feature entry $feature=$value to feature list $feature_list: $error"); return; } # If add_only is true and feature already exists, skip the update if ( $add_only && exists $features->{$feature} ) { say "Feature $feature already exists in $feature_list with value $features->{$feature}, skipping update" if $verbose; return; } $features->{$feature} = $value; # Instead of calling whmapi1 directly, we call the backend module to avoid additional overhead of whmapi1 calls # but ensure we still get the same result as if we were to call the API. # including has_root as 1 to bypass _check_reseller_permissions eval { Cpanel::Features::update_featurelist( $feature_list, $features, 'root', 1 ); }; if ( my $error = $@ ) { my $error_message = Cpanel::Exception::get_string_no_id($error); $error_message = "$error" if !defined $error_message; my $skip_message = "Skipping update for feature list $feature_list due to exception: $error_message"; logger()->warn($skip_message); say $skip_message if $verbose; return; } say "Feature $feature added to feature list $feature_list with default of $value" if $verbose; return 1; } =head2 do_once() Creates a touch file to track whether a task has been done. The task is executed and as long as the touch file exists, it will not do it again. =cut sub do_once (%opts) { return unless $opts{version} && $opts{code} && ref $opts{code} eq 'CODE'; my $lock = _lock_name(%opts); return if -e $lock; _mark_did_once(%opts); my $ret = eval { $opts{code}->(); }; warn($@) if $@; return $ret; } sub _mark_did_once (%opts) { my $lock = _lock_name(%opts); if ( !Cpanel::FileUtils::TouchFile::touchfile($lock) ) { warn("Failed to touch cpanel $opts{version} version file"); } return 1; } sub _lock_name (%opts) { return $VERSION_DIR . '/cpanel' . $opts{version}; } sub load_config_file ($file) { require Common::JSONC; return Common::JSONC::load_jsonc($file); } =head2 I<OBJ>->run() Runs this script. =cut sub run ($self) { my $config = {}; my $config_path = $self->getopt('config'); if ($config_path) { $config = load_config_file($config_path); } $config->{verbose} = $self->getopt('verbose') // !!0; $config->{flag} //= $self->getopt('flag') || die $self->full_help(); $config->{feature} //= $self->getopt('feature') || die $self->full_help(); # only allow one of these arguments if ( $self->getopt('enable') && $self->getopt('disable') ) { say "ERROR: Only one of --enable or --disable flags may be passed."; die $self->full_help(); } # fixed json deserialize issue $config->{enable} = !!1 if $config->{enable} && $config->{enable} eq 'true'; $config->{enable} = !!0 if $config->{enable} && $config->{enable} eq 'false'; # overwrite if set by command line $config->{enable} = !!1 if $self->getopt('enable'); $config->{enable} = !!0 if $self->getopt('disable'); die $self->full_help() if !defined $config->{enable}; # fixed json deserialize issue $config->{all} = !!1 if $config->{all} && $config->{all} eq 'true'; $config->{all} = !!0 if $config->{all} && $config->{all} eq 'false'; # overwrite if set by command line $config->{all} = !!1 if $self->getopt('all'); # fixed json deserialize issue for add_only $config->{add_only} = !!1 if $config->{add_only} && $config->{add_only} eq 'true'; $config->{add_only} = !!0 if $config->{add_only} && $config->{add_only} eq 'false'; $config->{add_only} //= !!0; # Default to false (existing behavior) $config->{list} = $self->getopt('list') if $self->getopt('list') && $self->getopt('list')->@*; die $self->full_help() if !$config->{all} && ( !$config->{list} || ref $config->{list} ne 'ARRAY' || !$config->{list}->@* ); my $make_changes = !!0; if ( $config->{condition} ) { require Cpanel::Plugins::Components::Rules; my $rules_obj = Cpanel::Plugins::Components::Rules->new( 'config' => $config->{condition} ); $make_changes = $rules_obj->is_allowed(); } else { $make_changes = !!1; say "No condition provided, proceeding with feature list modification" if $config->{verbose}; } if ( !$make_changes ) { logger()->info("Skipping feature list modification as per the conditional rule passed"); say "Skipping feature list modification as per the conditional rule that didn't match." if $config->{verbose}; return 0; } _verify_directories(); do_once( 'version' => $config->{flag}, 'eol' => 'never', 'code' => sub { my $verbose = $config->{verbose}; say "Running feature list modification for $config->{feature} with enable set to $config->{enable}"; if ( $config->{all} ) { modify_feature_for_all_feature_lists( $config->{feature}, $config->{enable} ? "1" : "0", $verbose, $config->{add_only} ); } else { foreach my $feature_list ( $config->{list}->@* ) { modify_featurelist( $feature_list, $config->{feature}, $config->{enable} ? "1" : "0", $verbose, $config->{add_only} ); } } }, ); return; } 1;