diff --git a/include/class-Domain.php b/include/class-Domain.php index a5bdcc2..d564529 100644 --- a/include/class-Domain.php +++ b/include/class-Domain.php @@ -1,284 +1,294 @@ . // 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 + * 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 ); + return Domain::permalink( $this->getDomainName(), $absolute ); + } + + /** + * Get the permalink to the edit plan page + * + * @param boolean $absolute True for an absolute URL + * @return string + */ + public function getDomainPlanPermalink( $absolute = false ) { + return Plan::domainPermalink( $this->getDomainName(), $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 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(); } /** * 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 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-DomainAPI.php b/include/class-DomainAPI.php index f17f614..d4fcb0a 100644 --- a/include/class-DomainAPI.php +++ b/include/class-DomainAPI.php @@ -1,160 +1,160 @@ . // load dependent traits class_exists( 'PlanAPI' ); /** * Methods related to a Domain class */ trait DomainAPITrait { use PlanAPITrait; /** * Where the domains are editable by me * * @return self */ public function whereDomainIsEditable() { if( ! has_permission( 'edit-domain-all' ) ) { $this->whereDomainUser(); } return $this; } /** * Where the Domain is Active (or not) * * @param boolean $active If you want the active, or the inactive * @return self */ public function whereDomainIsActive( $active = true ) { return $this->whereInt( 'domain_active', $active ); } /** * Limit to a certain user (or yourself) * * @param $user_ID int * @return self */ public function whereDomainUser( $user_ID = false ) { if( $user_ID === false ) { $user_ID = get_user( 'user_ID' ); } $this->joinDomainUser(); return $this->whereInt( 'domain_user.user_ID', $user_ID ); } /** * Limit to a certian domain name * * @param $domain_name string * @return self */ public function whereDomainName( $domain_name ) { return $this->whereStr( 'domain_name', $domain_name ); } /** * Constructor from a domain ID * * @param $domain_ID int * @return self */ public function whereDomainID( $domain_ID ) { return $this->whereInt( static::DOMAIN_ID, $domain_ID ); } /** * Constructor from a Domain object * * @param $domain object * @return self */ public function whereDomain( $domain ) { return $this->whereDomainID( $domain->getDomainID() ); } /** * Order by the Domain name * * @param string $direction DESC|ASC * @return self */ public function orderByDomainName( $direction = null ) { return $this->orderBy( 'domain_name', $direction ); } /** * Join domain and users (once) * * @return self */ public function joinDomainUser() { if( empty( $this->joinedDomainUser ) ) { $this->from( 'domain_user' ); $this->equals( 'domain_user.domain_ID', 'domain.domain_ID' ); $this->joinedDomainUser = true; } return $this; } /** * Join whatever table with the domain table * * @return self */ public function joinDomain() { return $this->joinOn( 'INNER', 'domain', static::DOMAIN_ID, 'domain.domain_ID' ); } } /** * Domain API */ class DomainAPI extends Query { use DomainAPITrait; /** * Univoque Domain ID column name */ const DOMAIN_ID = 'domain.domain_ID'; /** * Univoque Plan ID column name */ - const PLAN_ID = 'domain.plan_ID'; + protected $PLAN_ID = 'domain.plan_ID'; /** * Constructor * * @param object $db Database (or NULL for the current one) */ public function __construct( $db = null ) { // set database and class name parent::__construct( $db, 'Domain' ); // set database table $this->from( Domain::T ); } } diff --git a/include/class-Plan.php b/include/class-Plan.php index f112a3a..7e451cf 100644 --- a/include/class-Plan.php +++ b/include/class-Plan.php @@ -1,138 +1,173 @@ . // load Plan trait class_exists( 'Plan' ); /** * Methods for a Plan class */ trait PlanTrait { /** * Get plan ID * * @return int */ public function getPlanID() { return $this->get( 'plan_ID' ); } /* * Get plan name * * @return string */ public function getPlanName() { return $this->get( 'plan_name' ); } + /* + * Get the plan UID + * + * @return string + */ + public function getPlanUID() { + return $this->get( 'plan_uid' ); + } + /** * Get the number of FTP users of this Plan * * @return int|null */ public function getPlanFTPUsers() { return $this->get( 'plan_ftpusers' ); } /** * Get the number of Databases of this Plan * * @return int|null */ public function getPlanDatabases() { return $this->get( 'plan_databases' ); } /** * Get the number of Mailboxes of this Plan * * @return int|null */ public function getPlanMailboxes() { return $this->get( 'plan_mailboxes' ); } /** * Get the number of Mailforwardings of this Plan * * @return int|null */ public function getPlanMailforwardings() { return $this->get( 'plan_mailforwards' ); } + /** + * Get the Plan yearly price + * + * @return float + */ + public function getPlanYearlyPrice() { + return $this->get( 'plan_yearlyprice' ); + } + /** * Get the plan edit URl * * @param boolean $absolute True for an absolute URL * @return string */ public function getPlanPermalink( $absolute = false ) { return Plan::permalink( $this->get( 'plan_name' ), $absolute ); } /** * Normalize a Plan object after being retrieved from database */ protected function normalizePlan() { $this->integers( 'plan_ID', 'plan_ftpusers', 'plan_databases', 'plan_mailboxes', 'plan_mailforwards' ); + $this->floats( + 'plan_yearlyprice' + ); } } /** * Describe the 'plan' table */ class Plan extends Queried { use PlanTrait; /** * Table name */ const T = 'plan'; /** * Constructor */ public function __construct() { $this->normalizePlan(); } /** * Get the plan permalink * * @param string $plan_name Plan name - * @param boolean $absolute True for an absolute URL + * @param boolean $absolute True for an absolute URL */ public static function permalink( $plan_name = null, $absolute = false ) { $url = 'plan.php'; if( $plan_name ) { - $url .= _ . $plan_name; + $url .= "/$plan_name"; + } + return site_page( $url, $absolute ); + } + + /** + * Get the Domain's Plan permalink + * + * @param string $plan_name Plan name + * @param boolean $absolute True for an absolute URL + */ + public static function domainPermalink( $domain_name = null, $absolute = false ) { + $url = 'domain-plan.php'; + if( $domain_name ) { + $url .= "/$domain_name"; } return site_page( $url, $absolute ); } } diff --git a/include/class-PlanAPI.php b/include/class-PlanAPI.php index 65a4480..ab223c2 100644 --- a/include/class-PlanAPI.php +++ b/include/class-PlanAPI.php @@ -1,101 +1,101 @@ . /** * Methods related to a Plan class */ trait PlanAPITrait { /** * Limit to a specific Plan by its UID * - * @param $plan_name string + * @param string $uid Plan UID * @return self */ public function wherePlanUID( $uid ) { - return $this->whereStr( 'plan_uid', $plan_uid ); + return $this->whereStr( 'plan_uid', $uid ); } /** * Constructor from a plan ID * - * @param $plan_ID int + * @param int $id Plan ID * @return self */ - public function wherePlanID( $plan_ID ) { - return $this->whereInt( static::PLAN_ID, $plan_ID ); + public function wherePlanID( $id ) { + return $this->whereInt( $this->PLAN_ID, $id ); } /** * Constructor from a Plan object * * @param $plan object * @return self */ public function wherePlan( $plan ) { return $this->wherePlanID( $plan->getPlanID() ); } /** * Order by the Plan name * * @param string $direction DESC|ASC * @return self */ public function orderByPlanName( $direction = null ) { return $this->orderBy( 'plan_name', $direction ); } /** * Join whatever table with the plan table * * @param string $type Join type * @return self */ public function joinPlan( $type = 'INNER' ) { - return $this->joinOn( $type, 'plan', static::PLAN_ID, 'plan.plan_ID' ); + return $this->joinOn( $type, 'plan', $this->PLAN_ID, 'plan.plan_ID' ); } } /** * Plan API */ class PlanAPI extends Query { use PlanAPITrait; /** * Plan ID column name */ - const PLAN_ID = 'plan.plan_ID'; + protected $PLAN_ID = 'plan.plan_ID'; /** * Constructor * * @param object $db Database (or NULL for the current one) */ public function __construct( $db = null ) { // set database connection and default class name for the results parent::__construct( $db, 'Plan' ); // select from this database table $this->from( Plan::T ); } } diff --git a/load-post.php b/load-post.php index e91403f..ab06775 100644 --- a/load-post.php +++ b/load-post.php @@ -1,109 +1,112 @@ . // 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' ); +// default currency simbol +define_default( 'DEFAULT_CURRENCY_SYMBOL', '€' ); + // 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', "KISS 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/domain-plan-edit.php b/template/domain-plan-edit.php new file mode 100755 index 0000000..2eaa312 --- /dev/null +++ b/template/domain-plan-edit.php @@ -0,0 +1,76 @@ +. + +/* + * This is the template for the Domain Plan edit form + * + * Called from: + * www/domain-plan.php + * template/domain-plan-page.php + * + * Available variables: + * $domain Domain object + * $plan Plan object + */ + +// all the Plans +$plans = ( new PlanApi() ) + ->queryGenerator(); + +// unuseful when load directly +defined( 'BOZ_PHP' ) or die; +?> + + + + + +

+ + +
+ + + + +

+ +

+ + + +

+ +

+ +
+ + + + + diff --git a/template/domain-plan-overview.php b/template/domain-plan-overview.php new file mode 100644 index 0000000..efc9a04 --- /dev/null +++ b/template/domain-plan-overview.php @@ -0,0 +1,67 @@ +. + +/* + * This is the template to see Domain Plan info + * + * Called from: + * www/domain-plan.php + * template/domain-plan-page.php + * + * Available variables: + * $domain Domain object + * $plan Plan object + */ + +// unuseful when load directly +defined( 'BOZ_PHP' ) or die; +?> + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
getDomainName() ) ?>
getPlanName() ) ?>
getPlanMailboxes() ?>
getPlanMailforwardings() ?>
getPlanFTPUsers() ?>
getPlanYearlyPrice() ?>
+ + diff --git a/template/domain.php b/template/domain-plan-page.php similarity index 66% copy from template/domain.php copy to template/domain-plan-page.php index 88b4348..e495bd0 100644 --- a/template/domain.php +++ b/template/domain-plan-page.php @@ -1,45 +1,42 @@ . /* - * This is the template for the website domain dashboard page + * This is the template to see/change the Domain Plan * * Called from: - * domain.php + * template/domain-plan-page.php * * Available variables: - * $domain object Domain - * $plan object Plan + * $domain Domain object + * $plan Plan object */ // unuseful when load directly defined( 'BOZ_PHP' ) or die; -// pass the same arguments to the sub-templates +// some shared template arguments $args = [ 'domain' => $domain, 'plan' => $plan, ]; -// spawn the mailboxes list -template( 'mailboxes', $args ); +// show some information +template( 'domain-plan-overview', $args ); -// spawn the mail forwardings list -template( 'mailforwards', $args ); - -// spawn the ftp list -template( 'ftp-users', $args ); +// eventually show the edit form +template( 'domain-plan-edit', $args ); diff --git a/template/domain.php b/template/domain-plan-section.php old mode 100644 new mode 100755 similarity index 57% copy from template/domain.php copy to template/domain-plan-section.php index 88b4348..081f5db --- a/template/domain.php +++ b/template/domain-plan-section.php @@ -1,45 +1,59 @@ . /* - * This is the template for the website domain dashboard page + * This is the template to see/change the Domain Plan * * Called from: * domain.php * * Available variables: - * $domain object Domain - * $plan object Plan + * $domain Domain object + * $plan Plan object */ // unuseful when load directly defined( 'BOZ_PHP' ) or die; +?> +

-// pass the same arguments to the sub-templates -$args = [ - 'domain' => $domain, - 'plan' => $plan, -]; + getPlanID() ): ?> -// spawn the mailboxes list -template( 'mailboxes', $args ); + -// spawn the mail forwardings list -template( 'mailforwards', $args ); + getDomainPlanPermalink(), + $plan->getPlanName() + ) ?> -// spawn the ftp list -template( 'ftp-users', $args ); + + + + + + + getDomainPlanPermalink(), + __( "Assign" ) + ) ?> + + + + diff --git a/template/domain.php b/template/domain.php index 88b4348..bccec1c 100644 --- a/template/domain.php +++ b/template/domain.php @@ -1,45 +1,48 @@ . /* * This is the template for the website domain dashboard page * * Called from: * domain.php * * Available variables: * $domain object Domain * $plan object Plan */ // unuseful when load directly defined( 'BOZ_PHP' ) or die; // pass the same arguments to the sub-templates $args = [ 'domain' => $domain, 'plan' => $plan, ]; // spawn the mailboxes list template( 'mailboxes', $args ); // spawn the mail forwardings list template( 'mailforwards', $args ); // spawn the ftp list template( 'ftp-users', $args ); + +// show some links to the plan +template( 'domain-plan-section', $args ); diff --git a/www/domain-plan.php b/www/domain-plan.php new file mode 100644 index 0000000..7404f18 --- /dev/null +++ b/www/domain-plan.php @@ -0,0 +1,106 @@ +. + +/* + * This is the single e-mail forwarding edit page + */ + +// load framework +require '../load.php'; + +// wanted informations from the templates +$domain = null; +$plan = null; + +// URL paramenters (just domain) +list( $domain_name ) = url_parts( 1 ); + +// eventually retrieve domain from database +if( !$domain ) { + $domain = ( new DomainAPI() ) + ->select( [ + 'domain.domain_ID', + 'domain_name', + + 'plan.plan_ID', + 'plan_name', + 'plan_mailboxes', + 'plan_mailforwards', + 'plan_databases', + 'plan_ftpusers', + 'plan_yearlyprice', + ] ) + ->whereDomainName( $domain_name ) + ->joinPlan( 'LEFT' ) + ->queryRow(); + + // no domain no party + if( !$domain ) { + PageNotFound::spawn(); + } +} + +// save destination action +if( is_action( 'domain-plan-save' ) ) { + + // check privileges + require_permission( 'edit-domain-all' ); + + // check if the submitted request has sense + $new_plan_uid = $_POST['plan_uid'] ?? null; + if( !$new_plan_uid || !is_string( $new_plan_uid ) ) { + BadRequest::spawn(); + } + + // check if the new Plan exists + $new_plan = ( new PlanAPI() ) + ->wherePlanUID( $new_plan_uid ) + ->queryRow(); + + // no Plan no party + if( !$new_plan ) { + BadRequest::spawn( __( "Missing Plan" ) ); + } + + // finally update the Plan + ( new DomainAPI() ) + ->whereDomain( $domain ) + ->update( [ + 'plan_ID' => $new_plan->getPlanID(), + ] ); + + // POST -> REDIRECT -> GET + http_redirect( $domain->getDomainPlanPermalink() ); +} + +// spawn header +Header::spawn( [ + 'uid' => false, + 'title' => __( "Domain Plan" ), + 'breadcrumb' => [ + new MenuEntry( null, $domain->getDomainPermalink(), $domain->getDomainName() ), + ], +] ); + +// spawn the page content +template( 'domain-plan-page', [ + 'domain' => $domain, + 'plan' => $domain, +] ); + +// spawn the footer +Footer::spawn();