diff --git a/documentation/database/patches/patch-0006-log.sql b/documentation/database/patches/patch-0006-log.sql new file mode 100644 index 0000000..60ea128 --- /dev/null +++ b/documentation/database/patches/patch-0006-log.sql @@ -0,0 +1,19 @@ +CREATE TABLE `{$prefix}log` ( + `log_ID` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `log_family` VARCHAR(32) NOT NULL COMMENT 'What was this action about: mailbox, mailforward, domain.privilege.kick ecc.', + `log_action` VARCHAR(32) NOT NULL COMMENT 'What was the exact action: create, delete, change.password, ecc.', + `log_timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'When the action was registered', + `actor_ID` INT(10) UNSIGNED NOT NULL COMMENT 'The user that has done the action', + `marionette_ID` INT(10) UNSIGNED NULL COMMENT 'The user that was touched by the actor', + `mailforwardfrom_ID` INT(10) UNSIGNED NULL COMMENT 'The email forwarding touched by the actor', + `mailbox_ID` INT(10) UNSIGNED NULL COMMENT 'The mailbox touched by the actor', + `domain_ID` INT(10) UNSIGNED NULL COMMENT 'The domain touched by the actor', + `plan_ID` INT(10) UNSIGNED NULL COMMENT 'The plan touched by the actor', + PRIMARY KEY ( `log_ID` ), + KEY `idx_timestamp_actor_family` (`log_timestamp`,`actor_ID`,`log_family`), + CONSTRAINT `fk-{$prefix}log-actor` FOREIGN KEY ( `actor_ID` ) REFERENCES `{$prefix}user`(`user_ID`), + CONSTRAINT `fk-{$prefix}log-marionette` FOREIGN KEY ( `marionette_ID` ) REFERENCES `{$prefix}user`(`user_ID`), + CONSTRAINT `fk-{$prefix}log-mailforwardfrom` FOREIGN KEY ( `mailforwardfrom_ID` ) REFERENCES `{$prefix}mailforwardfrom`(`mailforwardfrom_ID`), + CONSTRAINT `fk-{$prefix}log-mailbox` FOREIGN KEY ( `mailbox_ID` ) REFERENCES `{$prefix}mailbox`(`mailbox_ID`), + CONSTRAINT `fk-{$prefix}log-plan` FOREIGN KEY ( `plan_ID` ) REFERENCES `{$prefix}plan`(`plan_ID`) +); diff --git a/include/class-APILog.php b/include/class-APILog.php new file mode 100644 index 0000000..fab0e3d --- /dev/null +++ b/include/class-APILog.php @@ -0,0 +1,150 @@ +. + +/** + * Class to interact with the `log` database table + */ +class APILog { + + /** + * @param array $args Arguments + */ + public static function insert( $args ) { + + // data to be saved + $data = []; + + // no family no party + if( !isset( $args['family'] ) ) { + throw new Exception( "missing family" ); + } + + // no action no party + if( !isset( $args['action'] ) ) { + throw new Exception( "missing action" ); + } + + // set the default Actor ID + if( isset( $args['actor'] ) ) { + $data['actor_ID'] = User::getID( $args['actor'] ); + } else { + + // otherwise please take the currently logged-in user as default + $data['actor_ID'] = get_user()->getUserID(); + } + + // you cannot change the timestamp + $data['log_timestamp'] = date( 'Y-m-d H:i:s' ); + + // set the family + $data['log_family'] = $args['family']; + + // set the action name + $data['log_action'] = $args['action']; + + // eventually set the Domain ID + if( isset( $args['domain'] ) ) { + $data['domain_ID'] = Domain::getID( $args['domain'] ); + } + + // eventually set the Mailbox ID + if( isset( $args['mailbox'] ) ) { + $data['mailbox_ID'] = Mailbox::getID( $args['mailbox'] ); + } + + // eventually set the Mailforwardfrom ID + if( isset( $args['mailforwardfrom'] ) ) { + $data['mailforwardfrom_ID'] = Mailforwardfrom::getID( $args['mailforwardfrom'] ); + } + + // eventually set the Plan ID + if( isset( $args['plan'] ) ) { + $data['plan_ID'] = Plan::getID( $args['plan'] ); + } + + // eventually set the marionette ID (the touched User's ID) + if( isset( $args['marionette'] ) ) { + $data['marionette_ID'] = User::getID( $args['marionette'] ); + } + + // finally insert the row + ( new QueryLog() ) + ->insertRow( $data ); + + } + + /** + * Help in querying stuff + * + * @param array $args Arguments + */ + public static function query( $args ) { + + // expected arguments defaults to NULL + $actor = $args['actor'] ?? null; + $marionette = $args['marionette'] ?? null; + $mailbox = $args['mailbox'] ?? null; + $domain = $args['domain'] ?? null; + + // create a fresh query builder + $query = new QueryLog(); + + // select the most important columns + $query->select( [ + 'log_timestamp', + 'log_family', + 'log_action', + ] ); + + // eventually filter by Actor (the user who was doing the action) + if( $actor ) { + $query->whereLogActor( $actor ); + } + + // eventually filter by Marionette (the user who was receiving an edit) + if( $marionette ) { + $query->whereLogMarionette( $marionette ); + } + + // eventually filter by Mailbox + if( $mailbox ) { + $query->whereMailbox( $mailbox ); + } + + // eventually filter by Domain + if( $domain ) { + $query->whereDomain( $domain ); + } + + // eventually skip to join something + $query->joinLogMessageTables( [ + 'actor' => is_object( $actor ), + 'marionette' => is_object( $marionette ), + 'mailbox' => is_object( $mailbox ), + 'domain' => is_object( $domain ), + ] ); + + // as default sort descending the timeline + $query->orderByLogTimestamp( 'DESC' ); + + // allow to change the limit + $query->limit( $args['limit'] ?? 15 ); + + return $query; + + } +} diff --git a/include/class-ActivityPanel.php b/include/class-ActivityPanel.php new file mode 100644 index 0000000..3fdc91a --- /dev/null +++ b/include/class-ActivityPanel.php @@ -0,0 +1,67 @@ +. + +/** + * Help in printing the content of an Activity Panel + */ +class ActivityPanel { + + /** + * Spawn the activity panel + * + * Allowed arguments: + * + * 'query' => [ query arguments ] + * The query arguments are the one available in APILog::query( $args ) + * + * @param array $args Associative array of arguments. + */ + public static function spawn( $args ) { + + // available entities that can be used as filter + $entities = [ + 'actor', + 'marionette', + 'mailbox', + 'domain', + 'mailforwardfrom', + ]; + + $query_args = $args['query'] ?? []; + + // build the query + $query = APILog::query( $query_args ); + + // if you filter by an Actor, the system will automatically NOT query Actor(s) in the log + // so, we pass that Actor to Log#getLogMessage( $args ) in order to give it that object + // to build the message + $message_args = []; + foreach( $entities as $entity ) { + $query_arg = $query_args[ $entity ] ?? null; + if( is_object( $query_arg ) ) { + $message_args[ $entity ] = $query_arg; + } + } + + + // spawn the activity panel + template( 'activity-panel', [ + 'message_args' => $message_args, + 'query' => $query, + ] ); + } +} diff --git a/include/class-Log.php b/include/class-Log.php new file mode 100644 index 0000000..fccb38b --- /dev/null +++ b/include/class-Log.php @@ -0,0 +1,141 @@ +. + +// make sure that this class is loaded at startup +class_exists( User::class ); +class_exists( Domain::class ); + +trait LogTrait { + + /** + * Get the log actor name + * + * @return string + */ + public function getLogActorFirm() { + return User::firm( $this->get( 'actor_uid' ) ); + } + + /** + * Get the action family + * + * @return string + */ + public function getLogFamily() { + return $this->get( 'log_family' ); + } + + /** + * Get the action + */ + public function getLogAction() { + return $this->get( 'log_action' ); + } + + /** + * Get the action + */ + public function getLogDate() { + return $this->get( 'log_timestamp' ); + } + + /** + * Get the log message + * + * @param array $args Arguments + * @return self + */ + public function getLogMessage( $args ) { + + $family = $this->getLogFamily(); + $action = $this->getLogAction(); + + // trigger the right message family + switch( $family ) { + case 'domain': + return self::domainMessage( $action, $this, $args ); + } + + return 'misterious action'; + } + + protected function normalizeLog() { + $this->datetimes( 'log_timestamp' ); + } +} + +/** + * A generic log of an action + * + * Something happened. Dunno what. + */ +class Log extends Queried { + + use LogTrait; + use UserTrait; + use DomainTrait; + + public function __construct() { + $this->normalizeLog(); + } + + /** + * Database table name + */ + const T = 'log'; + + /** + * Generate a Domain-related message + * + * @param string $action The related action name + * @param object $log + * @param array $args Arguments + * @return string Message + */ + public static function domainMessage( $action, $log, $args ) { + + /** + * You can pass some objects to build the message: + * + * A complete 'actor' User object + * A complete 'domain' Domain object + */ + $actor = $args['actor'] ?? $log; + $domain = $args['domain'] ?? $log; + $plan = $args['plan'] ?? $log; + + // create the Actor firm from the passed User object or from the Log + $actor_firm = $actor instanceof User + ? $actor->getUserFirm() + : $log->getLogActorFirm(); + + switch( $action ) { + + // an administrator has changed the Plan for a Domain + case 'plan.change': + return sprintf( + "%s - %s changed the Plan for %s to %s", + $log->getLogDate()->format( __( "Y-m-d H:i" ) ), + $actor_firm, + $domain->getDomainFirm(), + esc_html( $plan->getPlanName() ) + ); + } + + return 'edited a domain (wtf?)'; + } +} diff --git a/include/class-QueryLog.php b/include/class-QueryLog.php new file mode 100644 index 0000000..1ad52b8 --- /dev/null +++ b/include/class-QueryLog.php @@ -0,0 +1,200 @@ +. + +// make sure that this class is loaded +class_exists( MailboxAPI::class ); + +/** + * Methods of a QueryLog object + */ +trait QueryLogTrait { + + /** + * Filter to a specific log family + * + * @param string $family + * @return self + */ + public function whereLogFamily( $family ) { + return $this->whereStr( 'log_family', $family ); + } + + /** + * Filter to a specific actor + * + * The actor is the user who performed the action. + * + * @param object $user Actor + * @return self + */ + public function whereLogActor( $user ) { + return $this->whereLogActorID( $user->getUserID() ); + } + + /** + * Filter to a specific marionette User ID + * + * The marionette is the user touched by the actor. + * + * @param string $id User ID + * @return self + */ + public function whereLogMarionette( $id ) { + return $this->whereLogMarionetteID( $user->getUserID() ); + } + + /** + * Filter to a certain actor ID + * + * The actor is the user who performed the action. + * + * @param string $id User ID + * @return self + */ + public function whereLogActorID( $id ) { + return $this->whereInt( 'actor_ID', $id ); + } + + /** + * Filter to a specific marionette User ID + * + * The marionette is the user touched by the actor. + * + * @param string $id User ID + * @return self + */ + public function whereLogMarionetteID( $id ) { + return $this->whereInt( 'marionette_ID', $id ); + } + + /** + * Order by the log timestamp + * + * @param string $dir Direction + * @return self + */ + public function orderByLogTimestamp( $dir = 'DESC' ) { + return $this->orderBy( 'log_timestamp', $dir ); + } + + /** + * Join with the tables that are necessary to build the log message + * + * @param array $skip_join Array of entities to do not join + * @return self + */ + public function joinLogMessageTables( $skip_join = [] ) { + + // as default nothing is skipped + $skip_actor = $skip_join['actor'] ?? false; + $skip_marionette = $skip_join['marionette'] ?? false; + $skip_domain = $skip_join['domain'] ?? false; + $skip_mailbox = $skip_join['mailbox'] ?? false; + $skip_plan = $skip_join['plan'] ?? false; + $skip_mailforwardfrom = $skip_join['mailforwardfrom'] ?? false; + + // inner join with the User table to retrieve the actor (the user who has done the action) + if( !$skip_actor ) { + // type, table, first column, second column, table alias + $this->joinOn( 'INNER', 'user', 'actor_ID', 'actor.user_ID', 'actor' ); + $this->select( [ + 'actor_ID', + 'actor.user_uid AS actor_uid', + ] ); + } + + // left join with the User table to retrieve the marionette (the user affected by this action) + if( !$skip_marionette ) { + // type, table, first column, second column, table alias + $this->joinOn( 'LEFT', 'user', 'marionette_ID', 'marionette.user_ID', 'marionette' ); + $this->select( [ + 'marionette_ID', + 'marionette.user_uid AS marionette_uid', + ] ); + } + + // left join with the Domain table + if( !$skip_domain ) { + // type, table, first column, second column + $this->joinOn( 'LEFT', 'domain', 'domain.domain_ID', 'log.domain_ID' ); + $this->select( [ + 'domain.domain_ID', + 'domain_name', + ] ); + } + + // left join with the Mailbox table + if( !$skip_mailbox ) { + // type, table, first column, second column + $this->joinOn( 'LEFT', 'mailbox', 'mailbox.mailbox_ID', 'log.mailbox_ID' ); + $this->select( [ + 'mailbox_username', + ] ); + } + + // left join with the Mailforwardfrom table + if( !$skip_mailforwardfrom ) { + // type, table, first column, second column + $this->joinOn( 'LEFT', 'mailforwardfrom', 'mailforwardfrom.mailforwardfrom_ID', 'log.mailforwardfrom_ID' ); + } + + // left join with the Plan table + if( !$skip_plan ) { + // type, table, first column, second column + $this->joinOn( 'LEFT', 'plan', 'plan.plan_ID', 'log.plan_ID' ); + $this->select( [ + 'plan_name', + ] ); + } + + return $this; + } + +} + +/** + * Query the `log` database table + */ +class QueryLog extends Query { + + use QueryLogTrait; + use MailboxAPITrait; + + /** + * Univoque Domain ID column name + */ + const DOMAIN_ID = 'log.domain_ID'; + + /** + * Univoque column name to the Mailbox ID + * + * @var string + */ + protected $MAILBOX_ID = 'log.mailbox_ID'; + + /** + * Constructor + * + * @param object $db Database + */ + public function __construct( $db = null ) { + parent::__construct( $db, Log::class ); + + // default table + $this->from( Log::T ); + } +} diff --git a/load-post.php b/load-post.php index ca4a586..6f9e5a4 100644 --- a/load-post.php +++ b/load-post.php @@ -1,129 +1,129 @@ . /** * This is your versioned configuration file * * It does not contains secrets. * * This file is required after loading your * unversioned configuration file: * * load.php */ // database version // // you can increase your database version if you added some patches in: // documentation/database/patches -define( 'DATABASE_VERSION', 5 ); +define( 'DATABASE_VERSION', 6 ); // 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', ] );