diff --git a/include/class-Domain.php b/include/class-Domain.php index d4ba3fd..a5bdcc2 100644 --- a/include/class-Domain.php +++ b/include/class-Domain.php @@ -1,253 +1,284 @@ <?php # Copyright (C) 2018, 2019, 2020 Valerio Bozzolan # Boz Libre Hosting Panel # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. // load Plan trait class_exists( 'Plan' ); /** * Methods for a Domain class */ trait DomainTrait { use PlanTrait; /** * Count of the Domain's Mailboxes * * This is a kind of cache * * @var int */ private $domainMailboxCount = null; /** * Count of the Domain's FTP accounts * * This is a kind of cache * * @var int */ private $domainFTPAccountCount = null; /** * Get domain ID * * @return int */ public function getDomainID() { return $this->get( 'domain_ID' ); } /* * Get domain name * * @return string */ public function getDomainName() { return $this->get( 'domain_name' ); } + /** + * Get the sanitized relative directory name of this domain name + * + * Actually this should be valid for both the MTA and for the webserver. + * + * @return string + */ + public function getDomainDirname() { + + $dir = $this->getDomainName(); + + // it was validated during creation time, but validate also now + // to prevent malicious actions over hacked databases + sanitize_subdirectory( $dir ); + + return $dir; + } + /** * Get the domain edit URl * * @param boolean $absolute True for an absolute URL * @return string */ public function getDomainPermalink( $absolute = false ) { return Domain::permalink( $this->get( 'domain_name' ), $absolute ); } /** * Check if you can create a new Mailbox for this Domain * * The Domain must have Plan informations. * * @return boolean */ public function canCreateMailboxInDomain() { return $this->getPlanMailboxes() > $this->getDomainMailboxCount() || has_permission( 'edit-email-all' ); } /** * Check if you can create a new FTP account for this Domain * * The Domain must have Plan informations. * * The Domain must have Plan informations. */ public function canCreateFTPAccountForDomain() { return $this->getPlanFTPUsers() > $this->getDomainFTPAccountCount() || has_permission( 'edit-ftp-all' ); } /** * Factory mailbox from this domain * * @return MailboxFullAPI */ public function factoryMailbox() { return ( new MailboxFullAPI() )->whereDomain( $this ); } /** * Factory e-mail forward from this domain * * @return MailforwardFullAPI */ public function factoryMailforwardfrom() { return ( new MailforwardfromQuery() )->whereDomain( $this ); } /** * Set a count of Domain's Mailboxes * * This method should not be used directly. * * @param $count int * @return self */ public function setDomainMailboxCount( $count ) { $this->domainMailboxCount = $count; } /** * Get the number of Mailboxes of this Domain * * This method has a layer of cache. * * @return int */ public function getDomainMailboxCount() { // check if we already know the count if( !isset( $this->domainMailboxCount ) ) { // count the number of mailboxes associated to this Domain $count = $this->factoryMailbox() ->select( 'COUNT(*) count' ) ->queryValue( 'count' ); // save in cache $this->domainMailboxCount = (int) $count; } return $this->domainMailboxCount; } /** * Get the number of FTP accounts of this Domain * * This method has a layer of cache. * * @return int */ public function getDomainFTPAccountCount() { // check if we already know the count if( !isset( $this->domainFTPAccountCount ) ) { // count the number of mailboxes associated to this Domain $count = $this->factoryFTP() ->select( 'COUNT(*) count' ) ->queryValue( 'count' ); // save in cache $this->domainFTPAccountCount = (int) $count; } return $this->domainFTPAccountCount; } /** - * Get the MTA directory containing Domain's mailboxes + * Get the expected MTA directory containing Domain's mailboxes + * + * This pathname should be considered true for the MTA host. * * TODO: actually all the mailbox are on the same host. * Then, we should support multiple hosts. * * @return string */ public function getDomainMailboxesPath() { + // mailboxes are stored under a $BASE/domain/username filesystem structure + return MAILBOX_BASE_PATH . __ . $this->getDomainDirname(); + } - // require a valid filename or throw - $domain_name = $this->getDomainName(); - require_safe_filename( $domain_name ); - + /** + * Get the expected and sanitized base domain directory containing its directories + * + * This pathname should be considered true both for the webserver serving + * that domain and for the related FTP server. + * + * TODO: actually all the domains are on the same host. + * Then, we should support multiple hosts. + * + * @return string + */ + public function getDomainBasePath() { // mailboxes are stored under a $BASE/domain/username filesystem structure - return MAILBOX_BASE_PATH . __ . $domain_name; + return VIRTUALHOSTS_DIR . __ . $this->getDomainDirname(); } /** * Factory FTP users from this domain * * @return FTPAPI */ public function factoryFTP() { return ( new FTPAPI() )->whereDomain( $this ); } /** * Normalize a Domain object after being retrieved from database */ protected function normalizeDomain() { $this->integers( 'domain_ID' ); $this->booleans( 'domain_active' ); $this->dates( 'domain_born', 'domain_expiration' ); $this->normalizePlan(); } } /** * Describe the 'domain' table */ class Domain extends Queried { use DomainTrait; /** * Table name */ const T = 'domain'; const UID = 'domain_name'; /** * Constructor */ public function __construct() { $this->normalizeDomain(); } /** * Get the domain permalink * * @param string $domain_name Domain name * @param boolean $absolute True for an absolute URL */ public static function permalink( $domain_name = null, $absolute = false ) { $url = 'domain.php'; if( $domain_name ) { $url .= _ . $domain_name; } return site_page( $url, $absolute ); } } diff --git a/include/class-FTP.php b/include/class-FTP.php index 7fb31f5..972e913 100644 --- a/include/class-FTP.php +++ b/include/class-FTP.php @@ -1,148 +1,184 @@ <?php -# Copyright (C) 2019 Valerio Bozzolan +# Copyright (C) 2019, 2020 Valerio Bozzolan # Boz Libre Hosting Panel # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. // load dependent traits class_exists( 'Domain' ); /** * Methods for an FTP class */ trait FTPTrait { use DomainTrait; /** * Get the FTP login username * * @return string E-mail */ public function getFTPLogin() { return $this->get( 'ftp_login' ); } /** * Check if this FTP account is active * * @return bool */ public function isFTPActive() { return $this->get( 'ftp_active' ); } + /** + * Get the sanitized raw directory + * + * This value is sanitized before saving but it can be a malicious string + * if the database is overtaken so it's sanitized again. + * + * @return string + */ + public function getFTPRawDirectory() { + + $path = $this->get( 'ftp_directory' ); + + // even if it was sanitized during creation, double-sanitize + // to protect against hacked databases + validate_subdirectory( $path ); + + return $path; + } + + /** + * Get the sanitized absolute FTP directory pathname + * + * @return string + */ + public function getFTPAbsoluteDirectory() { + + // get the sanitized domain base path + $absolute_base = $this->getDomainBasePath(); + + // get the sanitized FTP raw directory + $relative_path = $this->getFTPRawDirectory(); + + // at this point the absolute realpath of the subdirectory is verified + // to do not contain '/../' or other crap + return $absolute_base . __ . $relative_path; + } + /** * Get the mailbox permalink * * @return string */ public function getFTPPermalink( $absolute = false ) { return FTP::permalink( $this->getDomainName(), $this->getFTPLogin() ); } protected function normalizeFTP() { $this->normalizeDomain(); $this->booleans( 'ftp_active' ); $this->integers( 'ftp_ulbandwidth', 'ftp_dlbandwidth', 'ftp_quotasize', 'ftp_quotafiles' ); } } /** * An FTP user */ class FTP extends Queried { use FTPTrait; /** * Table name */ const T = 'ftp'; /** * Constructor * * Normalize the object obtained from the database */ public function __construct() { $this->normalizeFTP(); } /** * Get the FTP permalink from domain name and FTP login * * @param string $domain Domain name * @param string $login FTP user login * @return string */ public static function permalink( $domain, $login = null ) { $url = sprintf( '%s/%s', ROOT . '/ftp.php', $domain ); if( $login ) { $url .= "/$login"; } return $url; } /** * Encrypt an FTP user password * * @param $password string Clear text password * @return string One-way encrypted password */ public static function encryptPassword( $password ) { global $HOSTING_CONFIG; // the FTP password encryption mechanism can be customized if( isset( $HOSTING_CONFIG->FTP_ENCRYPT_PWD ) ) { return call_user_func( $HOSTING_CONFIG->FTP_ENCRYPT_PWD, $password ); } // or then just a default behaviour /** * The default behaviour is to adopt the crypt() encryption mechanism * with SHA512 and some random salt. It's strong enough nowadays. * * Read your FTP server documentation, whatever you are using. * We don't know how your infrastructure works, so we don't know * how you want your password encrypted in the database and what kind * of password encryption mechanisms your FTP server supports. * * In short if you are using PureFTPd this default configuration may work * because you may have PureFTPd configured as follow: * ... * MYSQLCrypt crypt * ... * * You can read more here: * https://download.pureftpd.org/pub/pure-ftpd/doc/README.MySQL * * Anyway you can use whatever FTP server that talks with a MySQL database * and so you should adopt the most stronger encryption mechanism available. */ $salt = bin2hex( openssl_random_pseudo_bytes( 3 ) ); return '{SHA512-CRYPT}' . crypt( $password, "$6$$salt" ); } - } diff --git a/include/functions.php b/include/functions.php index 126dd82..69c8859 100644 --- a/include/functions.php +++ b/include/functions.php @@ -1,231 +1,251 @@ <?php # Copyright (C) 2018, 2019, 2020 Valerio Bozzolan # Reyboz another self-hosting panel project # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. /** * Require a certain page from the template directory * * @param $name string page name (to be sanitized) * @param $args mixed arguments to be passed to the page scope */ function template( $template_name, $template_args = [] ) { extract( $template_args, EXTR_SKIP ); return require TEMPLATE_PATH . __ . "$template_name.php"; } /** * Get the returned text from a template * * @param $name string page name (to be sanitized) * @param $args mixed arguments to be passed to the page scope * @see template() * @return string */ function template_content( $name, $args = [] ) { ob_start(); template( $name, $args ); $text = ob_get_contents(); ob_end_clean(); return $text; } /** * Print an e-mail (safe for bots) * * @param $email string */ function email_blur( $email ) { $dot = strip_tags( __( " dot <!-- . -->" ) ); $at = strip_tags( __( " at <!-- @ -->" ) ); $email = esc_html( $email ); echo str_replace( [ '.', '@' ], [ $dot, $at ], $email ); } /** * Send an e-mail to someone * * @param $subject string E-mail subject * @param $message string E-mail message * @param $to string E-mail recipient (from current logged-in user as default) */ function send_email( $subject, $message, $to = false ) { if( ! $to ) { if( ! is_logged() ) { die( "can't retrieve e-mail address from anon user" ); } $to = get_user( 'user_email' ); } return SMTPMail::instance() ->to( $to ) ->message( $subject, $message ) ->disconnect(); } /** * Require a certain permission * * @param $permission string An internal permission like 'edit-all-user' * @param $redirect boolean Enable or disable the redirect */ function require_permission( $permission, $redirect = true ) { if( ! has_permission( $permission ) ) { require_more_privileges( $redirect ); } } /** * Require more privileges then actual ones */ function require_more_privileges( $redirect = true ) { if( is_logged() ) { Header::spawn( [ 'title' => __( "Permission denied" ), ] ); Footer::spawn(); exit; } else { $login = menu_entry( 'login' ); $url = $login->getAbsoluteURL(); if( $redirect && isset( $_SERVER[ 'REQUEST_URI' ] ) ) { $url = http_build_get_query( $url, [ 'redirect' => $_SERVER[ 'REQUEST_URI' ], ] ); } http_redirect( $url, 307 ); } } /** * Get URL parts from the PATH_INFO * * It spawn a "bad request" page if something goes wrong. * * @param $max int If $min is specified, this is the maximum number of parameters. When unspecified, this is the exact number of parameters. * @param $min int Mininum number of parameters. * @return array * @see https://httpd.apache.org/docs/2.4/mod/core.html#acceptpathinfo */ function url_parts( $max, $min = false ) { if( $min === false ) { $min = $max; } // split the PATH_INFO parts $parts = explode( _, $_SERVER['PATH_INFO'] ?? '' ); array_shift( $parts ); // eventually spawn the "bad request" $n = count( $parts ); if( $n > $max || $n < $min ) { BadRequest::spawn( __( "unexpected URL" ) ); } // eventually fill expected fields for( $i = $n; $i < $max; $i++ ) { $parts[] = null; } return $parts; } /** * Link to an existing page from the menu * * @param $uid string E.g. 'index' * @param $args mixed Arguments */ function the_menu_link( $uid, $args = [] ) { $page = menu_entry( $uid ); the_link( $page->getURL(), $page->name, $args ); } /** * Link to a whatever page * * P.S. link() is a reserved function * * @param $url string * @param $title string * @param $args mixed Arguments */ function the_link( $url, $title, $args = [] ) { template( 'link', [ 'title' => $title, 'url' => $url, 'args' => $args, ] ); } /** * Generate a password * * @param $bytes int */ function generate_password( $bytes = 8 ) { return rtrim( base64_encode( bin2hex( openssl_random_pseudo_bytes( $bytes ) ) ), '=' ); } /** * Validate a mailbox username * * @param $mailbox string * @return bool */ function validate_mailbox_username( $mailbox ) { return 1 === preg_match( '/^[a-z][a-z0-9-_.]+$/', $mailbox ); } /** * A certain value must be an e-mail * * @param $email string * @return string filtered e-mail */ function require_email( $email ) { $email = luser_input( $email, 128 ); if( filter_var( $email, FILTER_VALIDATE_EMAIL ) === false ) { BadRequest::spawn( __( "fail e-mail validation" ) ); } return $email; } /** * Require a safe filename or throw an exception * - * It returns that filename untouched. - * * This method is designed to prevend known filesystem exploitations * that may occurr if a database is compromised and with malicous * data inside domain names, etc. * * Just don't try to register a domain name with a slash in the name. * * @param string $filename - * @return string */ function require_safe_filename( $filename ) { - $bads = [ '/', '\\', '..' ]; foreach( $bads as $bad ) { - if( strpos( $filename, '/' ) !== false ) { + if( strpos( $filename, $bad ) !== false ) { throw new Exception( sprintf( "found '%s' in the string '%s': it cannot be considered a safe file/directory name", $bad, $filename ) ); } } +} - return $filename; +/** + * Check if a directory is under a subdirectory or throw + * + * @param string $sub_directory Specific sub-directory or just '/' + */ +function validate_subdirectory( $sub_directory ) { + + // validate each part of this pathname + // note that '' and '/' are allowed and also '///' because + // Unix already accepts it and resolve '/base////suffix' + // It does NOT accept stuff like '/../../' + $parts = explode( '/', $sub_directory ); + foreach( $parts as $part ) { + try { + require_safe_filename( $part ); + } catch( Exception $e ) { + // give a meaningful exception + throw new Exception( sprintf( + "unable to sanitize '%s': %s", + $sub_directory, + $e->getMessage() + ) ); + } + } } diff --git a/load-post.php b/load-post.php index 289e558..c7d61ef 100644 --- a/load-post.php +++ b/load-post.php @@ -1,103 +1,109 @@ <?php # Copyright (C) 2018 Valerio Bozzolan # Boz Libre Hosting Panel # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. // include path define_default( 'INCLUDE_PATH', ABSPATH . __ . 'include' ); // template path define_default( 'TEMPLATE_PATH', ABSPATH . __ . 'template' ); // autoload classes from the /include directory spl_autoload_register( function( $name ) { // TODO: autoload classes and create DomainTrait and use in Mailbox $path = INCLUDE_PATH . __ . "class-$name.php"; if( is_file( $path ) ) { require $path; } } ); // override default user class define( 'SESSIONUSER_CLASS', 'User' ); // load common functions require INCLUDE_PATH . __ . 'functions.php'; // jquery URL // provided by the libjs-jquery package as default define_default( 'JQUERY_URL', '/javascript/jquery/jquery.min.js' ); // Bootstrap CSS/JavaScript files without trailing slash // provided by the libjs-bootstrap package as default define_default( 'BOOTSTRAP_DIR_URL', '/javascript/bootstrap' ); // path to the Net SMTP class // provided by the php-net-smtp package as default define_default( 'NET_SMTP', '/usr/share/php/Net/SMTP.php' ); +// base directory for your virtualhosts +// e.g. you may have /var/www/example.com/index.html +// do NOT end with a slash +// TODO: support multiple hosts +define_default( 'VIRTUALHOSTS_DIR', '/var/www' ); + // register JavaScript/CSS files register_js( 'jquery', JQUERY_URL ); register_js( 'bootstrap', BOOTSTRAP_DIR_URL . '/js/bootstrap.min.js' ); register_css( 'bootstrap', BOOTSTRAP_DIR_URL . '/css/bootstrap.min.css' ); register_css( 'custom-css', ROOT . '/content/style.css' ); // GNU Gettext i18n define( 'GETTEXT_DOMAIN', 'reyboz-hosting-panel' ); define( 'GETTEXT_DIRECTORY', 'l10n' ); define( 'GETTEXT_DEFAULT_ENCODE', CHARSET ); // UTF-8 // common strings define_default( 'SITE_NAME', "Boz Libre Hosting Panel" ); define_default( 'CONTACT_EMAIL', 'support@' . DOMAIN ); define_default( 'REPO_URL', 'https://gitpull.it/project/profile/15/' ); // limit session duration to 5 minutes (60s * 100m) define_default( 'SESSION_DURATION', 6000 ); /** * Mailbox base path * * Used by CLI scripts to calculate the current quotas. * * The mailboxes should have paths like: * MAILBOX_BASE_PATH/domain_name/user_name/ */ define_default( 'MAILBOX_BASE_PATH', '/home/vmail' ); // register web pages add_menu_entries( [ new MenuEntry( 'index', '/', __( "Dashboard" ), null, 'backend' ), new MenuEntry( 'login', 'login.php', __( "Login" ) ), new MenuEntry( 'profile', 'profile.php', __( "Profile" ) ), new MenuEntry( 'logout', 'logout.php', __( "Logout" ), null, 'read' ), new MenuEntry( 'user-list', 'user-list.php', __( "Users" ), null, 'edit-user-all' ), new MenuEntry( 'password-reset', 'password-reset.php', __( "Password reset" ) ), ] ); // permissions of a normal user register_permissions( 'user', [ 'read', 'backend', ] ); // permissions of an admin inherit_permissions( 'admin', 'user', [ 'edit-user-all', 'edit-email-all', 'edit-domain-all', 'edit-plan-all', 'edit-ftp-all', ] ); diff --git a/template/ftp.php b/template/ftp.php index 117e112..ba4556f 100644 --- a/template/ftp.php +++ b/template/ftp.php @@ -1,77 +1,108 @@ <?php -# Copyright (C) 2019 Valerio Bozzolan +# Copyright (C) 2019, 2020 Valerio Bozzolan # Boz Libre Hosting Panel # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. /* * This is the template for a single FTP account * * Called from: * ftp.php * * Available variables: * $domain Domain object * $ftp FTP object */ // unuseful when load directly defined( 'BOZ_PHP' ) or die; ?> <?php template( 'ftp-description' ) ?> + <!-- access instructions --> <?php if( $ftp ): ?> <?php template( 'ftp-access', [ 'domain' => $domain, 'ftp' => $ftp, ] ) ?> + <?php endif ?> + <!-- /access instructions --> - <h3><?= __( "Actions" ) ?></h3> + <!-- create/edit form --> + <form method="post"> + <?php form_action( 'ftp-save' ) ?> - <!-- delete form --> - <form method="post"> - <?php form_action( 'ftp-delete' ) ?> - <?= HTML::input( 'hidden', 'ftp_login', $ftp->getFTPLogin() ) ?> - <p><button type="submit" class="btn btn-danger"><?= __( "Delete" ) ?></button></p> - </form> - <!-- /delete form --> + <p> + <label for="ftp-login"><?= __( "FTP login:") ?></label><br /> + <input type="text" id="ftp-login" name="ftp_login"<?php + if( $ftp ) { + echo value( $ftp->getFTPLogin() ); + echo " readonly"; + } + ?> /> + </p> + + <p> + <label for="ftp-directory"><?= __( "Sub-Directory:") ?></label><br /> + <input type="text" id="ftp-directory" name="ftp_directory" placeholder="/"<?php + if( $ftp ) { + echo value( $ftp->getFTPRawDirectory() ); + } + ?> /> + </p> + <p class="tip"><?= __( "Tip:" ) ?> + <em><?= __( "You may want to change the default Sub-Directory to restrict this FTP user to a specific pathname." ) ?> + <?= __( "Anyway, you may break the login of this FTP user if you don't know what you are doing." ) ?> + </em> + </p> + <p> + <button type="submit" class="btn btn-default"><?= + $ftp ? __( "Save" ) + : __( "Create" ) + ?></button> + </p> + </form> + <!-- /create/edit form --> + + <!-- other actions --> + <?php if( $ftp ): ?> + <h3><?= __( "Password" ) ?></h3> <!-- change password form --> <form method="post"> <?php form_action( 'ftp-password-reset' ) ?> <?php if( $ftp_password ): ?> <label for="password"><?= __( "Please copy your new password:" ) ?><br /> <input type="text" id="password" readonly<?= value( $ftp_password ) ?> /> <?php else: ?> <p><button type="submit" class="btn btn-default"><?= __( "Generate new password" ) ?></button></p> <?php endif ?> </form> <!-- /change password form --> - <?php else: ?> + <h3><?= __( "Actions" ) ?></h3> - <!-- create form --> + <!-- delete form --> <form method="post"> - <?php form_action( 'ftp-save' ) ?> - <p> - <label for="ftp-login"><?= __( "FTP login:") ?></label><br /> - <input type="text" id="ftp-login" name="ftp_login" /> - <button type="submit" class="btn btn-default"><?= __( "Create" ) ?></button> - </p> + <?php form_action( 'ftp-delete' ) ?> + <?= HTML::input( 'hidden', 'ftp_login', $ftp->getFTPLogin() ) ?> + <p><button type="submit" class="btn btn-danger"><?= __( "Delete" ) ?></button></p> </form> - <!-- /create form --> + <!-- /delete form --> <?php endif ?> + <!-- /other actions --> diff --git a/www/ftp.php b/www/ftp.php index d3b36f9..75316e3 100644 --- a/www/ftp.php +++ b/www/ftp.php @@ -1,179 +1,208 @@ <?php -# Copyright (C) 2019 Valerio Bozzolan +# Copyright (C) 2019, 2020 Valerio Bozzolan # Boz Libre Hosting Panel # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. /* * This is the single FTP account creation/edit page */ // load framework require '../load.php'; // wanted informations $domain = null; $ftp = null; $ftp_password = null; // URL paramenters (maximum both domain and FTP login, minimum just domain) list( $domain_name, $ftp_login ) = url_parts( 2, 1 ); // eventually retrieve mailforward from database if( $ftp_login ) { $ftp = ( new FTPAPI() ) ->select( [ 'domain.domain_ID', 'domain_name', 'ftp_login', + 'ftp_directory', ] ) ->joinFTPDomain() ->whereDomainName( $domain_name ) ->whereFTPLogin( $ftp_login ) ->whereDomainIsEditable() ->queryRow(); // 404 $ftp or PageNotFound::spawn(); // recycle the mailforward object that has domain informations $domain = $ftp; } // eventually retrieve domain from database if( ! $domain ) { $domain = ( new DomainAPI() ) ->select( [ 'domain.domain_ID', 'domain.domain_name', ] ) ->whereDomainName( $domain_name ) ->whereDomainIsEditable() ->queryRow(); // 404 $domain or PageNotFound::spawn(); } if( ! $ftp ) { // to create an FTP user, must edit all FTP users require_permission( 'edit-ftp-all' ); } // save destination action if( is_action( 'ftp-save' ) ) { - // save source only during creation - if( ! $ftp ) { + $data = []; - // sanitize data + // during creation require FTP login + if( !$ftp ) { if( !isset( $_POST['ftp_login'] ) || !is_string( $_POST['ftp_login'] ) ) { BadRequest::spawn( __( "missing parameter" ) ); } - // generate the username (must start with domain name) - $username = generate_slug( $domain->getDomainName() ) . '_' . $_POST[ 'ftp_login' ]; + // generate the username - MUST start with domain name + $username = generate_slug( $domain->getDomainName() ) . '_' . $_POST['ftp_login']; $username = luser_input( $username, 128 ); // validate username if( !validate_mailbox_username( $username ) ) { - BadRequest::spawn( __( "invalid mailbox name" ) ); + BadRequest::spawn( __( "invalid username" ) ); } // check existence $ftp_exists = ( new FTPAPI() ) ->select( 1 ) ->whereDomain( $domain ) ->whereFTPLogin( $username ) ->queryRow(); // die if exists if( $ftp_exists ) { BadRequest::spawn( __( "FTP account already existing" ) ); } - // generate a random password and die (probably the User will not see it because of the redirect) - $ftp_password = generate_password(); - $ftp_password_safe = FTP::encryptPassword( $ftp_password ); + // save these fields + $data['domain_ID'] = $domain->getDomainID(); + $data['ftp_login'] = $username; + } else { + $username = $ftp->getFTPLogin(); + } - // insert as new row - insert_row( 'ftp', [ - new DBCol( 'domain_ID', $domain->getDomainID(), 'd' ), - new DBCol( 'ftp_login', $username, 's' ), - new DBCol( 'ftp_password', $ftp_password_safe, 's' ), - ] ); + // no FTP directory no party + $ftp_directory = $_POST['ftp_directory'] ?? null; + + // validate the FTP directory and save or die + try { + validate_subdirectory( $ftp_directory ); + } catch( Exception $e ) { + error_log( $e->getMessage() ); + BadRequest::spawn( sprintf( + __( "invalid Sub-Directory: %s" ), + $e->getMessage() + ) ); + } - // POST/redirect/GET - http_redirect( FTP::permalink( - $domain->getDomainName(), - $username, - true - ), 303 ); + // at this point the directory is safe + $data['ftp_directory'] = $ftp_directory ?? null; + + // during creation generate a random password + if( !$ftp ) { + $ftp_password = generate_password(); + $data['ftp_password'] = FTP::encryptPassword( $ftp_password ); } + + // insert or update + if( $ftp ) { + ( new FTPAPI() ) + ->whereFTP( $ftp ) + ->update( $data ); + } else { + ( new FTPAPI() ) + ->insertRow( $data ); + } + + // POST/redirect/GET + http_redirect( FTP::permalink( + $domain->getDomainName(), + $username, + true + ), 303 ); } // change password action if( $ftp && is_action( 'ftp-password-reset' ) ) { // generate a password and die $ftp_password = generate_password(); $ftp_password_safe = FTP::encryptPassword( $ftp_password ); // update its password ( new FTPAPI() ) ->whereFTP( $ftp ) ->update( [ 'ftp_password' => $ftp_password_safe, ] ); } // delete action if( $ftp ) { // action fired when deleting a whole mailforward if( is_action( 'ftp-delete' ) ) { // delete the account ( new FTPAPI() ) ->whereFTP( $ftp ) ->delete(); // POST/redirect/GET http_redirect( $domain->getDomainPermalink( true ), 303 ); } } // spawn header Header::spawn( [ 'uid' => false, 'title-prefix' => __( "FTP user" ), 'title' => $ftp ? $ftp->getFTPLogin() : __( "create" ), 'breadcrumb' => [ new MenuEntry( null, $domain->getDomainPermalink(), $domain->getDomainName() ), ], ] ); // spawn the page content template( 'ftp', [ 'domain' => $domain, 'ftp' => $ftp, 'ftp_password' => $ftp_password, ] ); // spawn the footer Footer::spawn();