HEX
Server: Apache
System: Linux hz.vslconceptsdomains.com 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64
User: dkfounda (3233)
PHP: 8.1.34
Disabled: exec,passthru,shell_exec,system
Upload Files
File: //scripts/create_default_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::create_default_featurelist;

use cPstrict;

=encoding utf-8

=head1 NAME

create_default_featurelist - Create default feature list with a specified feature and its dependencies

=head1 USAGE

    create_default_featurelist --feature <feature_name> [--preferred-featurelist-name <name>] [--verbose] [--help]

=head1 DESCRIPTION

This script creates a default feature list that includes a specified feature and all
of its dependencies. The feature list is created using the WHM API v1 C<create_featurelist>
function.

This is a generic script that can be used by any plugin to create its default feature list.
The script will check if the feature list already exists before attempting to create it,
and will skip creation if it is already present on the system.

=head1 OPTIONS

=over 4

=item B<--feature>, B<-f> (required)

The name of the feature to include in the feature list. This feature and all of its
dependencies will be added to the created feature list.

=item B<--preferred-featurelist-name>, B<-p> (optional)

The preferred name for the feature list. If not provided, the feature name will be used.
The actual feature list name will be automatically prefixed with C<__default_>.

For example:

=over 4

=item * C<--feature standalone-pal --preferred-featurelist-name nova> creates C<__default_nova>

=item * C<--feature standalone-pal> (no preferred name) creates C<__default_standalone-pal>

=back

=item B<--verbose>, B<-v>

Enable verbose output. When enabled, the script will output detailed information about
the feature list creation process, including:

=over 4

=item * Feature names and dependencies being processed

=item * Existing feature lists on the system

=item * The exact whmapi1 command being executed

=item * Success and status messages

=back

=item B<--help>, B<-h>

Display the help message and exit.

=back

=head1 EXAMPLES

=over 4

=item Create feature list for standalone-pal (silent mode)

    create_default_featurelist --feature standalone-pal

=item Create feature list with custom name

    create_default_featurelist --feature standalone-pal --preferred-featurelist-name nova

=item Create feature list with verbose output

    create_default_featurelist --feature standalone-pal --verbose
    create_default_featurelist -f standalone-pal -v

=item Create feature list with custom name and verbose output

    create_default_featurelist -f standalone-pal -p nova -v

=item Show help

    create_default_featurelist --help

=back

=head1 EXIT STATUS

The script exits with status 0 on success. If the feature list creation fails,
the script exits with status 1 and outputs error messages to STDERR.

=head1 DEPENDENCIES

=over 4

=item * L<Cpanel::Features::Metadata> - To load feature dependencies

=item * L<Cpanel::Features::Lists> - To check existing feature lists

=item * L<Cpanel::SafeRun::Object> - To execute whmapi1 safely

=item * L<Getopt::Long> - For command-line option parsing

=back

=head1 SEE ALSO

L<whmapi1>, L<Cpanel::Features::Metadata>

=cut

use Cpanel::Features::Metadata ();
use Cpanel::SafeRun::Object    ();
use Getopt::Long               ();

use constant WHMAPI1_PATH => '/usr/local/cpanel/bin/whmapi1';

our $VERBOSE = 0;

run() if !caller;

sub run {

    my $FEATURE_NAME;
    my $PREFERRED_FEATURELIST_NAME;
    my $DEFAULT_FEATURELIST_NAME;

    # Parse command line options
    Getopt::Long::GetOptions(
        'verbose|v'                      => \$VERBOSE,
        'help|h'                         => sub { print_help(); exit 0; },
        'feature|f=s'                    => \$FEATURE_NAME,
        'preferred-featurelist-name|p=s' => \$PREFERRED_FEATURELIST_NAME,

    ) or die "Invalid options\n";

    if ( !$FEATURE_NAME ) {
        die "Feature name is required. Use --feature <feature_name> to specify it.\n";
    }

    # Validate feature name
    if ( length($FEATURE_NAME) < 1 || length($FEATURE_NAME) > 255 ) {
        die "Feature name must be between 1 and 255 characters.\n";
    }

    # Validate feature name characters
    if ( $FEATURE_NAME !~ /\A[a-zA-Z0-9_-]+\z/ ) {
        die "Feature name contains invalid characters. Only alphanumeric characters, underscores (_), and hyphens (-) are allowed.\n";
    }

    # Validate preferred featurelist name if provided
    if ( defined($PREFERRED_FEATURELIST_NAME) ) {
        if ( length($PREFERRED_FEATURELIST_NAME) < 1 || length($PREFERRED_FEATURELIST_NAME) > 255 ) {
            die "Preferred featurelist name must be between 1 and 255 characters.\n";
        }
        if ( $PREFERRED_FEATURELIST_NAME !~ /\A[a-zA-Z0-9_-]+\z/ ) {
            die "Preferred featurelist name contains invalid characters. Only alphanumeric characters, underscores (_), and hyphens (-) are allowed.\n";
        }
    }

    # default it to feature name if preferred featurelist name is not provided
    my $featurelist_name = $PREFERRED_FEATURELIST_NAME // $FEATURE_NAME;
    $DEFAULT_FEATURELIST_NAME = get_default_featurelist_name($featurelist_name);

    _verbose("Starting feature list creation for Nova");
    _verbose( "Feature name: " . $FEATURE_NAME );
    _verbose( "Feature list name: " . $DEFAULT_FEATURELIST_NAME );

    # verify that if the feature list already exists
    if ( verify_if_featurelist_exists($DEFAULT_FEATURELIST_NAME) ) {
        _verbose("Feature list already exists, exiting");
        return 0;
    }

    # Populate the feature list for NOVA
    _verbose("Populating feature list with dependencies");

    # populate feature list will always include FEATURE_NAME in the list
    # even if there are no dependencies
    my $feature_list = populate_feature_list($FEATURE_NAME);
    return 0 unless $feature_list;

    _verbose( "Found " . scalar( keys %$feature_list ) . " features to add" );

    # execute whmapi1 to create the feature list
    _verbose("Creating feature list via whmapi1");
    return create_feature_list( $DEFAULT_FEATURELIST_NAME, $feature_list );
}

=head2 populate_feature_list

Populates the feature list hash with the main feature and its dependencies.

This function loads the feature metadata for the specified feature and retrieves all
of its dependencies. The behavior differs based on whether a metadata file exists:

=over 4

=item * If metadata file exists and loads successfully: Returns the main feature plus all dependencies

=item * If metadata file exists but fails to load: Logs an error and returns undef (skips feature list creation)

=item * If metadata file does not exist: Returns only the main feature with no dependencies

=back

=head3 Parameters

=over 4

=item * C<$feature_name> - The name of the feature to load

=back

=head3 Returns

=over 4

=item * Hashref where keys are feature names and values are 1, representing enabled features

=item * C<undef> if metadata file exists but fails to load (indicates creation should be skipped)

=back

=head3 Example

    my $features = populate_feature_list('standalone-pal');
    # Returns: { 'standalone-pal' => 1, 'dependency1' => 1, 'dependency2' => 1 }
    # Or: { 'standalone-pal' => 1 } if no metadata file exists
    # Or: undef if metadata exists but is corrupted

=cut

sub populate_feature_list($feature_name) {
    my $metadata = Cpanel::Features::Metadata->new( feature => $feature_name );
    my $features = eval { $metadata->load()->dependencies; } // [];

    if ( my $exception = $@ ) {

        if ( $metadata->is_metadata_exists ) {

            # something wrong with metadata load, so skip creating a default feature list
            warn "Failed to load feature metadata for '$feature_name': '$exception'\n";
            warn "Skipping default feature list creation due to metadata load failure.\n";
            return;
        }
        else {
            _verbose("The metadata file does not exist, creating feature list with main feature only");
        }

    }
    _verbose( "Dependencies loaded: " . join( ', ', @{$features} ) ) if @{$features};
    push @$features, $feature_name;

    # convert to hash
    my %feature_hash = map { $_ => 1 } @{$features};
    return \%feature_hash;
}

=head2 create_feature_list

Creates the default feature list using the WHM API v1.

This function constructs the appropriate whmapi1 command with all the features
to be enabled and executes it using L<Cpanel::SafeRun::Object>. It handles
execution failures by logging errors to STDERR.

=head3 Parameters

=over 4

=item * C<$featurelist_name> - The name of the feature list to create

=item * C<$feature_list> - Hashref of features to enable (keys are feature names, values are 1)

=back

=head3 Returns

=over 4

=item * C<0> - Success (Unix exit code convention)

=item * C<1> - Failure (Unix exit code convention)

=back

=head3 Example

    my $result = create_feature_list('__default_nova', { 'standalone-pal' => 1, 'feature2' => 1 });
    if ($result == 0) {
        print "Feature list created successfully\n";
    }

=cut

sub create_feature_list ( $featurelist_name, $feature_list ) {

    my @args = ( 'create_featurelist', 'featurelist=' . $featurelist_name );

    foreach my $feature ( keys %$feature_list ) {
        push @args, "$feature=1";
    }

    _verbose( "Executing: " . WHMAPI1_PATH . " " . join( ' ', @args ) );

    my $result = Cpanel::SafeRun::Object->new(
        program => WHMAPI1_PATH,
        args    => \@args,
    );

    if ( $result->exec_failed ) {
        warn "Failed to execute whmapi1 for feature list '$featurelist_name'.\n";
        warn $result->stderr . "\n";
        return 1;
    }

    _verbose("Feature list created successfully");
    _verbose( "Output: " . $result->stdout ) if $result->stdout;
    return 0;
}

sub get_default_featurelist_name($feature_name) {

    # Adding unique name to avoid conflicts
    return '__default_' . $feature_name;
}

sub verify_if_featurelist_exists($featurelist_name) {
    require Cpanel::Features::Lists;
    _verbose("Checking if feature list already exists");
    my @existing_featurelists = Cpanel::Features::Lists::get_feature_lists(1);

    _verbose( "Existing feature lists: " . join( ', ', @existing_featurelists ) );

    if ( grep { $_ eq $featurelist_name } @existing_featurelists ) {
        warn "Feature list " . $featurelist_name . " already exists. Skipping creation.\n";
        return 1;
    }

    # feature list is not yet available, so lets create one
    return 0;
}

sub _verbose ($message) {
    return unless $VERBOSE;
    say "[VERBOSE] $message";
}

sub print_help {
    print <<"END_HELP";
Usage: $0 --feature <feature_name> [OPTIONS]

Create a default feature list with the specified feature and its dependencies.

Required:
    -f, --feature <name>                      The feature to include in the feature list

Options:
    -p, --preferred-featurelist-name <name>   Preferred name for the feature list (optional)
                                              If not provided, uses the feature name
                                              Final name will be prefixed with '__default_'
    -v, --verbose                             Enable verbose output
    -h, --help                                Show this help message

Examples:
    $0 --feature standalone-pal                                    # Create feature list
    $0 -f standalone-pal -p nova                                   # Custom feature list name
    $0 --feature standalone-pal --verbose                          # With verbose output
    $0 -f standalone-pal -p nova -v                                # All options

END_HELP
}

1;