diff --git a/include/class-Domain.php b/include/class-Domain.php index 5238d68..6c0f0ae 100644 --- a/include/class-Domain.php +++ b/include/class-Domain.php @@ -1,320 +1,357 @@ . // load Plan trait class_exists( Plan::class, true ); class_exists( MTA::class, true ); /** * 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 a printable Domain firm * * It may be a link if you are allowed to edit this Domain. * * @return string */ public function getDomainFirm() { return HTML::a( $this->getDomainPermalink(), esc_html( $this->getDomainName() ) ); } /** * 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 require_safe_dirname( $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->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 Mailforward for this Domain + * + * The Domain must have Plan informations. + * + * @return boolean + */ + public function canCreateMailforwardfromInDomain() { + return $this->getPlanMailforwardings() > $this->getDomainMailforwardfromCount() + || + 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 + * Factory a MailboxFullAPI from this Domain * * @return MailboxFullAPI */ public function factoryMailbox() { return ( new MailboxFullAPI() )->whereDomain( $this ); } /** - * Factory e-mail forward from this domain + * Factory a Mailforwardfrom from this Domain * - * @return MailforwardFullAPI + * @return MailforwardfromQuery */ 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 Mailforwardfrom addresses of this Domain + * + * This method has a layer of cache. + * + * @return int + */ + public function getDomainMailforwardfromCount() { + + // check if we already know the count + if( !isset( $this->domainMailforwardfromCount ) ) { + + // count the number of mailboxes associated to this Domain + $count = $this->factoryMailforwardfrom() + ->select( 'COUNT(*) count' ) + ->queryValue( 'count' ); + + // save in cache + $this->domainMailforwardfromCount = (int) $count; + } + + return $this->domainMailforwardfromCount; + } + /** * 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 VIRTUALHOST_BASE_PATH . __ . $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; use MTATrait; /** * Table name */ const T = 'domain'; const UID = 'domain_name'; /** * Constructor */ public function __construct() { $this->normalizeDomain(); $this->normalizeMTA(); } /** * 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 ); } /** * Force to get a Domain ID, whatever is passed * * @param mixed $domain Domain object or Domain ID * @return int */ public static function getID( $domain ) { return is_object( $domain ) ? $domain->getDomainID() : (int)$domain; } } diff --git a/include/class-Plan.php b/include/class-Plan.php index a7df264..b441506 100644 --- a/include/class-Plan.php +++ b/include/class-Plan.php @@ -1,225 +1,225 @@ . // 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 mailbox quota in bytes * * @return int */ public function getPlanMailboxQuota() { return $this->get( 'plan_mailboxquota' ); } /** * 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', 'plan_mailboxquota', ); $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 */ public static function permalink( $plan_name = null, $absolute = false ) { $url = 'plan.php'; if( $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 ); } /** * Get a percentage from two values * * It's always between 0 and 100, or NULL if cannot be calculated. * * @param $current int Current amount * @param $max int Maximum amount * @param $overflow bool Set to TRUE to allow to go over 100% * @return mixed Percentage or NULL */ public static function percentage( $current, $max, $overflow = false ) { // no args no party if( !$current || !$max ) { return null; } // cannot be negative if( $current < 0 ) { return 0; } // calculate the percentage $percentage = $current * 100 / $max; // you may don't want to go over 100% if( $percentage > 100 && !$overflow ) { return 100; } return (int) $percentage; } /** * Force to get a Plan ID, whatever is passed * * @param mixed $plan Plan object or Plan ID * @return int */ public static function getID( $plan ) { return is_object( $plan ) ? $plan->getPlanID() : (int)$plan; } } diff --git a/template/mailforward.php b/template/mailforward.php index 45d9f67..864e911 100644 --- a/template/mailforward.php +++ b/template/mailforward.php @@ -1,73 +1,73 @@ . /* * This is the template for a single e-mail forwarding * * Called from: * mailforward.php * * Available variables: * $domain Domain object * $mailforwardfrom Mailforward object */ // unuseful when load directly defined( 'BOZ_PHP' ) or die; ?>


getMailforwardfromAddress() ) ?> /> getMailforwardfromUsername() ); } ?> /> @getDomainName() ) ?>

getMailforwardfromAddress() ) ?>

$domain, 'mailforwardfrom' => $mailforwardfrom, ] ) ?> diff --git a/template/mailforwards.php b/template/mailforwards.php index c055270..816f844 100644 --- a/template/mailforwards.php +++ b/template/mailforwards.php @@ -1,73 +1,76 @@ . /* * This is the template for the mailboxes list * * Called from: * template/domain.php * * Available variables: * $domain object */ // unuseful when load directly defined( 'BOZ_PHP' ) or die; // domain mail forwardings $mailforwardfroms = $domain->factoryMailforwardfrom() ->joinDomain() ->select( [ 'mailforwardfrom.mailforwardfrom_ID', 'mailforwardfrom_username', 'domain_name', ] ) ->orderBy( 'mailforwardfrom_username' ) ->queryGenerator(); ?>

valid() ): ?>

getPlanName(), $plan->getPlanMailForwardings(), __( "Forwarding" ) ) ) ?>

getDomainName() ), - __( "Create" ) + __( "Create" ), + [ + 'disabled' => !$domain->canCreateMailforwardfromInDomain(), + ] ) ?>

diff --git a/www/mailforward.php b/www/mailforward.php index 71c9a22..83b4bf2 100644 --- a/www/mailforward.php +++ b/www/mailforward.php @@ -1,242 +1,244 @@ . /* * This is the single e-mail forwarding edit page */ // load framework require '../load.php'; // this page is not public require_permission( 'backend' ); // wanted informations $domain = null; $mailforwardfrom = null; // URL paramenters (maximum both domain and mailforward source, minimum just domain) list( $domain_name, $mailforwardfrom_username ) = url_parts( 2, 1 ); // eventually retrieve mailforward from database if( $mailforwardfrom_username ) { $mailforwardfrom = ( new MailforwardfromQuery() ) - ->select( [ - 'domain.domain_ID', - 'domain_name', - 'mailforwardfrom.mailforwardfrom_ID', - 'mailforwardfrom_username', - ] ) ->joinDomain() + ->joinPlan( 'LEFT' ) ->whereDomainName( $domain_name ) ->whereMailforwardfromUsername( $mailforwardfrom_username ) ->whereDomainIsEditable() ->queryRow(); // 404 $mailforwardfrom or PageNotFound::spawn(); // recycle the mailforward object that has domain informations $domain = $mailforwardfrom; } // eventually retrieve domain from database -if( ! $domain ) { +if( !$domain ) { $domain = ( new DomainAPI() ) - ->select( [ - 'domain.domain_ID', - 'domain.domain_name', - ] ) ->whereDomainName( $domain_name ) ->whereDomainIsEditable() + ->joinPlan( 'LEFT' ) ->queryRow(); // 404 $domain or PageNotFound::spawn(); } +// check if I'm creating a new one +if( !$mailforwardfrom ) { + + // check if I can add another one + if( !$domain->canCreateMailforwardfromInDomain() ) { + BadRequest::spawnPlanDoesNotAllow(); + } +} + + // save destination action if( is_action( 'mailforward-save' ) ) { // save source only during creation if( ! $mailforwardfrom ) { // sanitize if( ! isset( $_POST[ 'mailforwardfrom_username' ] ) ) { BadRequest::spawn( __( "missing parameter" ) ); } $username = luser_input( $_POST[ 'mailforwardfrom_username' ], 128 ); if( ! validate_mailbox_username( $username ) ) { BadRequest::spawn( __( "invalid mailbox name" ) ); } // check existence $mailforwardfrom_exists = ( new MailforwardfromQuery() ) ->select( 1 ) ->whereDomain( $domain ) ->whereMailforwardfromUsername( $username ) ->queryRow(); // die if exists if( $mailforwardfrom_exists ) { BadRequest::spawn( __( "e-mail forwarding already existing" ) ); } query( 'START TRANSACTION' ); // insert as new row insert_row( 'mailforwardfrom', [ new DBCol( 'domain_ID', $domain->getDomainID(), 'd' ), new DBCol( 'mailforwardfrom_username', $username, 's' ), ] ); // remember this action in the registry APILog::insert( [ 'family' => 'mailforward', 'action' => 'create', 'domain' => $domain, 'mailforwardfrom' => last_inserted_ID(), ] ); query( 'COMMIT' ); // POST/redirect/GET http_redirect( Mailforwardfrom::permalink( $domain->getDomainName(), $username, true ), 303 ); } } // delete action if( $mailforwardfrom ) { // action fired when deleting a whole mailforward if( is_action( 'mailforward-delete' ) ) { query( 'START TRANSACTION' ); // mark a deletion on this Domain APILog::insert( [ 'family' => 'mailforward', 'action' => 'delete', 'domain' => $domain, ] ); // drop the foreign key constraints // See https://gitpull.it/T518 ( new QueryLog() ) ->whereMailforwardfrom( $mailforwardfrom ) ->update( [ 'mailforwardfrom_ID' => null, ] ); // drop the existing one ( new MailforwardfromQuery() ) ->whereMailforwardfrom( $mailforwardfrom ) ->delete(); query( 'COMMIT' ); // POST/redirect/GET http_redirect( $domain->getDomainPermalink( true ), 303 ); } // action fired when adding/removing a mailforward if( ( is_action( 'mailforwardto-add' ) || is_action( 'mailforwardto-remove' ) ) && isset( $_POST[ 'address' ] ) ) { $address = require_email( $_POST[ 'address' ] ); if( $address === $mailforwardfrom->getMailforwardfromAddress() ) { BadRequest::spawn( __( "do not try to create a loop" ) ); } $existing_address = ( new MailforwardtoAPI() ) ->whereMailforwardfrom( $mailforwardfrom ) ->whereMailforwardtoAddress( $address ) ->queryRow(); // action fired when removing a mailforward if( is_action( 'mailforwardto-remove' ) && $existing_address ) { query( 'START TRANSACTION' ); // TODO refactor with query builder query( sprintf( "DELETE FROM %s WHERE mailforwardfrom_ID = %d and mailforwardto_address = '%s'", T( 'mailforwardto' ), $mailforwardfrom->getMailforwardfromID(), esc_sql( $address ) ) ); // remember that we removed an address APILog::insert( [ 'family' => 'mailforward', 'action' => 'remove.destination', 'domain' => $domain, 'mailforwardfrom' => $mailforwardfrom, ] ); query( 'COMMIT' ); } // action fired when adding a mailforward if( is_action( 'mailforwardto-add' ) && ! $existing_address ) { query( 'START TRANSACTION' ); insert_row( 'mailforwardto', [ new DBCol( 'mailforwardfrom_ID', $mailforwardfrom->getMailforwardfromID(), 'd' ), new DBCol( 'mailforwardto_address', $address, 's' ), ] ); // remember that we added an address APILog::insert( [ 'family' => 'mailforward', 'action' => 'add.destination', 'domain' => $domain, 'mailforwardfrom' => $mailforwardfrom, ] ); query( 'COMMIT' ); } } } // spawn header Header::spawn( [ 'uid' => false, 'title-prefix' => __( "E-mail forwarding" ), 'title' => $mailforwardfrom ? $mailforwardfrom->getMailforwardfromAddress() : __( "create" ), 'breadcrumb' => [ new MenuEntry( null, $domain->getDomainPermalink(), $domain->getDomainName() ), ], ] ); // spawn the page content template( 'mailforward', [ 'domain' => $domain, 'mailforwardfrom' => $mailforwardfrom, ] ); // spawn the footer Footer::spawn();