diff --git a/config.ini b/config.ini index c3c911dd..efbb6baf 100644 --- a/config.ini +++ b/config.ini @@ -68,7 +68,10 @@ changeUserEmail.all=admin changeUserAccessRank=admin changeUserName=moderator acceptUserRegistration=moderator -banUser=admin +banUser.own=nobody +banUser.all=admin +deleteUser.own=registered +deleteUser.all=nobody listComments=anonymous listTags=anonymous diff --git a/public_html/media/css/core.css b/public_html/media/css/core.css index 17b16765..b661c46f 100644 --- a/public_html/media/css/core.css +++ b/public_html/media/css/core.css @@ -101,6 +101,15 @@ body { border: 0; } +footer { + text-align: center; + margin-top: 1em; + padding-top: 0.5em; + border-top: 1px solid #eee; + font-size: small; + color: silver; +} + .clear { display: block; clear: both; @@ -111,7 +120,6 @@ body { width: 256px; margin-right: 2em; } - #sidebar .sidebar-unit { margin: 0 0 1.5em 0; padding: 0.75em; @@ -119,24 +127,40 @@ body { padding-left: 0; border-left: 0; } - #sidebar h1 { margin-top: 0; } -h1, h2, h3 { - font-weight: normal; + +#sidebar .key { + padding-right: 0.5em; +} +#sidebar .key-value { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } #inner-content { overflow: hidden; } +h1, h2, h3 { + font-weight: normal; +} + p:first-child, h1:first-child { margin-top: 0; } +hr { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAIElEQVQYlWNgQAPv3r37j4wHXBJdATY5BnyScAX4TAUAdfp5IbOdz/YAAAAASUVORK5CYII='); + border: 0; + height: 4px; +} + a { color: firebrick; text-decoration: none; @@ -202,7 +226,6 @@ form.aligned input[type=checkbox] { label { display: inline-block; } - label, input, select, @@ -210,9 +233,13 @@ button { font-family: inherit; font-size: 11pt; } -input:not([type=radio], [type=checkbox], [type=file]) { +select, +input:not([type=radio]):not([type=checkbox]):not([type=file]) { border: 1px solid #ccc; } +ul.tagit input { + border: 0 !important; +} button { font-size: 115%; padding: 0.2em 0.7em; @@ -235,6 +262,10 @@ button:hover { margin: 0 auto; } +.alert-success { + border-color: #aba; + background-color: #aea; +} .alert-error { border-color: #faa; background-color: #fdd; @@ -245,21 +276,6 @@ button:hover { background-color: #ffd; } -footer { - text-align: center; - margin-top: 1em; - padding-top: 0.5em; - border-top: 1px solid #eee; - font-size: small; - color: silver; -} - .inactive { opacity: .5; } - -hr { - background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAIElEQVQYlWNgQAPv3r37j4wHXBJdATY5BnyScAX4TAUAdfp5IbOdz/YAAAAASUVORK5CYII='); - border: 0; - height: 4px; -} diff --git a/public_html/media/css/post-view.css b/public_html/media/css/post-view.css index 27252413..7729cb85 100644 --- a/public_html/media/css/post-view.css +++ b/public_html/media/css/post-view.css @@ -69,10 +69,6 @@ i.icon-dl { content: ', '; } -.details .key { - margin-right: 0.5em; -} - .options ul { list-style-type: none; margin: 0; diff --git a/public_html/media/css/user-view.css b/public_html/media/css/user-view.css index c0e1bbbf..508d2cdb 100644 --- a/public_html/media/css/user-view.css +++ b/public_html/media/css/user-view.css @@ -1,16 +1,13 @@ #sidebar { width: 200px; -} - -.details .key { - margin-right: 0.5em; + font-size: 90%; } .tabs ul { list-style-type: none; margin: 0 0 1em 0; padding: 0; - border-bottom: 1px solid #eee; + border-bottom: 1px solid #ccc; } .tabs li { display: inline-block; @@ -24,11 +21,11 @@ .tabs li a { border: 1px solid white; - border-bottom: 1px solid #eee; + border-bottom: 1px solid #ccc; color: silver; } .tabs li.selected a { - border: 1px solid #eee; + border: 1px solid #ccc; border-bottom: 1px solid white; color: inherit; } @@ -46,3 +43,7 @@ form.aligned label.left { width: 10em; } + +form.edit .alert { + margin: 1em 0; +} diff --git a/public_html/media/js/core.js b/public_html/media/js/core.js index 0dd503ee..9c2323d7 100644 --- a/public_html/media/js/core.js +++ b/public_html/media/js/core.js @@ -1,3 +1,7 @@ +$.fn.hasAttr = function(name) { + return this.attr(name) !== undefined; +}; + if ($.when.all === undefined) { $.when.all = function(deferreds) @@ -39,4 +43,46 @@ $(function() } }); }); + + function confirmEvent(e) + { + if (!confirm($(this).attr('data-confirm-text'))) + { + e.preventDefault(); + e.stopPropagation(); + } + } + + $('form[data-confirm-text]').submit(confirmEvent); + $('a[data-confirm-text]').click(confirmEvent); + + $('a.simple-action').click(function(e) + { + if(e.isPropagationStopped()) + return; + + e.preventDefault(); + + var aDom = $(this); + if (aDom.hasClass('inactive')) + return; + aDom.addClass('inactive'); + + var url = $(this).attr('href') + '?json'; + $.get(url, function(data) + { + if (data['success']) + { + if (aDom.hasAttr('data-redirect-url')) + window.location.href = aDom.attr('data-redirect-url'); + else + window.location.reload(); + } + else + { + alert(data['errorMessage']); + aDom.removeClass('inactive'); + } + }); + }); }); diff --git a/public_html/media/js/post-view.js b/public_html/media/js/post-view.js index d556582b..b3073274 100644 --- a/public_html/media/js/post-view.js +++ b/public_html/media/js/post-view.js @@ -1,61 +1,5 @@ $(function() { - $('.add-fav a, .rem-fav a, .hide a, .unhide a').click(function(e) - { - e.preventDefault(); - - var aDom = $(this); - if (aDom.hasClass('inactive')) - return; - aDom.addClass('inactive'); - - var url = $(this).attr('href') + '?json'; - $.get(url, function(data) - { - if (data['success']) - { - window.location.reload(); - } - else - { - alert(data['errorMessage']); - aDom.removeClass('inactive'); - } - }); - }); - - $('.delete a').click(function(e) - { - e.preventDefault(); - - var aDom = $(this); - if (aDom.hasClass('inactive')) - return; - aDom.addClass('inactive'); - - //todo: move this string literal to html - if (confirm(aDom.attr('data-confirm-text'))) - { - var url = $(this).attr('href') + '?json'; - $.get(url, function(data) - { - if (data['success']) - { - window.location.href = aDom.attr('data-redirect-url'); - } - else - { - alert(data['errorMessage']); - aDom.removeClass('inactive'); - } - }); - } - else - { - aDom.removeClass('inactive'); - } - }); - $('li.edit a').click(function(e) { e.preventDefault(); diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php index fb4d7e6e..c2199e6d 100644 --- a/src/Controllers/AuthController.php +++ b/src/Controllers/AuthController.php @@ -1,12 +1,6 @@ registration->salt; - return sha1($salt1 . $salt2 . $pass); - } - /** * @route /auth/login */ @@ -23,21 +17,24 @@ class AuthController return; } - $suppliedUser = InputHelper::get('user'); - $suppliedPass = InputHelper::get('pass'); - if ($suppliedUser !== null and $suppliedPass !== null) + $suppliedName = InputHelper::get('name'); + $suppliedPassword = InputHelper::get('password'); + if ($suppliedName !== null and $suppliedPassword !== null) { - $dbUser = R::findOne('user', 'name = ?', [$suppliedUser]); + $dbUser = R::findOne('user', 'name = ?', [$suppliedName]); if ($dbUser === null) throw new SimpleException('Invalid username'); - $suppliedPassHash = self::hashPassword($suppliedPass, $dbUser->pass_salt); - if ($suppliedPassHash != $dbUser->pass_hash) + $suppliedPasswordHash = Model_User::hashPassword($suppliedPassword, $dbUser->pass_salt); + if ($suppliedPasswordHash != $dbUser->pass_hash) throw new SimpleException('Invalid password'); if (!$dbUser->staff_confirmed and $this->config->registration->staffActivation) throw new SimpleException('Staff hasn\'t confirmed your registration yet'); + if ($dbUser->banned) + throw new SimpleException('You are banned'); + if (!$dbUser->email_confirmed and $this->config->registration->emailActivation) throw new SimpleException('You haven\'t confirmed your e-mail address yet'); @@ -74,72 +71,53 @@ class AuthController return; } - $suppliedUser = InputHelper::get('user'); - $suppliedPass1 = InputHelper::get('pass1'); - $suppliedPass2 = InputHelper::get('pass2'); + $suppliedName = InputHelper::get('name'); + $suppliedPassword1 = InputHelper::get('password1'); + $suppliedPassword2 = InputHelper::get('password2'); $suppliedEmail = InputHelper::get('email'); - $this->context->suppliedUser = $suppliedUser; - $this->context->suppliedPass1 = $suppliedPass1; - $this->context->suppliedPass2 = $suppliedPass2; + $this->context->suppliedName = $suppliedName; + $this->context->suppliedPassword1 = $suppliedPassword1; + $this->context->suppliedPassword2 = $suppliedPassword2; $this->context->suppliedEmail = $suppliedEmail; $regConfig = $this->config->registration; - $passMinLength = intval($regConfig->passMinLength); - $passRegex = $regConfig->passRegex; - $userNameMinLength = intval($regConfig->userNameMinLength); - $userNameRegex = $regConfig->userNameRegex; $emailActivation = $regConfig->emailActivation; $staffActivation = $regConfig->staffActivation; $this->context->transport->staffActivation = $staffActivation; $this->context->transport->emailActivation = $emailActivation; - if ($suppliedUser !== null) + if ($suppliedName !== null) { - $dbUser = R::findOne('user', 'name = ?', [$suppliedUser]); - if ($dbUser !== null) - { - if (!$dbUser->email_confirmed) - throw new SimpleException('User with this name is already registered and awaits e-mail confirmation'); + $suppliedName = Model_User::validateUserName($suppliedName); - if (!$dbUser->staff_confirmed) - throw new SimpleException('User with this name is already registered and awaits admin confirmation'); - - throw new SimpleException('User with this name is already registered'); - } - - if (strlen($suppliedUser) < $userNameMinLength) - throw new SimpleException(sprintf('User name must have at least %d characters', $userNameMinLength)); - - if (!preg_match($userNameRegex, $suppliedUser)) - throw new SimpleException('User name contains invalid characters'); - - if ($suppliedPass1 != $suppliedPass2) + if ($suppliedPassword1 != $suppliedPassword2) throw new SimpleException('Specified passwords must be the same'); + $suppliedPassword = Model_User::validatePassword($suppliedPassword1); - if (strlen($suppliedPass1) < $passMinLength) - throw new SimpleException(sprintf('Password must have at least %d characters', $passMinLength)); - - if (!preg_match($passRegex, $suppliedPass1)) - throw new SimpleException('Password contains invalid characters'); - + $suppliedEmail = Model_User::validateEmail($suppliedEmail); if (empty($suppliedEmail) and $emailActivation) throw new SimpleException('E-mail address is required - you will be sent confirmation e-mail.'); - if (!empty($suppliedEmail) and !TextHelper::isValidEmail($suppliedEmail)) - throw new SimpleException('E-mail address appears to be invalid'); - - //register the user $dbUser = R::dispense('user'); - $dbUser->name = $suppliedUser; + $dbUser->name = $suppliedName; $dbUser->pass_salt = md5(mt_rand() . uniqid()); - $dbUser->pass_hash = self::hashPassword($suppliedPass1, $dbUser->pass_salt); + $dbUser->pass_hash = Model_User::hashPassword($suppliedPassword, $dbUser->pass_salt); $dbUser->email = $suppliedEmail; $dbUser->join_date = time(); - $dbUser->staff_confirmed = $staffActivation ? false : true; - $dbUser->email_confirmed = $emailActivation ? false : true; - $dbUser->access_rank = R::findOne('user') === null ? AccessRank::Admin : AccessRank::Registered; + if (R::findOne('user') === null) + { + $dbUser->staff_confirmed = true; + $dbUser->email_confirmed = true; + $dbUser->access_rank = AccessRank::Admin; + } + else + { + $dbUser->staff_confirmed = false; + $dbUser->email_confirmed = false; + $dbUser->access_rank = AccessRank::Registered; + } //prepare unique registration token do diff --git a/src/Controllers/PostController.php b/src/Controllers/PostController.php index b2da4300..dece6ed4 100644 --- a/src/Controllers/PostController.php +++ b/src/Controllers/PostController.php @@ -414,6 +414,7 @@ class PostController $edited = false; $secondary = $post->uploader->id == $this->context->user->id ? 'own' : 'all'; + $this->context->transport->post = $post; /* safety */ $suppliedSafety = InputHelper::get('safety'); diff --git a/src/Controllers/UserController.php b/src/Controllers/UserController.php index 77253017..199ae1c2 100644 --- a/src/Controllers/UserController.php +++ b/src/Controllers/UserController.php @@ -22,20 +22,160 @@ class UserController + /** + * @route /user/{name}/ban + * @validate name [^\/]+ + */ + public function banAction($name) + { + $user = self::locateUser($name); + $secondary = $user->id == $this->context->user->id ? 'own' : 'all'; + PrivilegesHelper::confirmWithException($this->context->user, Privilege::BanUser, $secondary); + $user->banned = true; + R::store($user); + $this->context->transport->success = true; + } + + /** + * @route /post/{name}/unban + * @validate name [^\/]+ + */ + public function unbanAction($name) + { + $user = self::locateUser($name); + $secondary = $user->id == $this->context->user->id ? 'own' : 'all'; + PrivilegesHelper::confirmWithException($this->context->user, Privilege::BanUser, $secondary); + $user->banned = false; + R::store($user); + $this->context->transport->success = true; + } + + /** + * @route /post/{name}/accept-registration + * @validate name [^\/]+ + */ + public function acceptRegistrationAction($name) + { + $user = self::locateUser($name); + PrivilegesHelper::confirmWithException($this->context->user, Privilege::AcceptUserRegistration); + $user->staff_confirmed = true; + R::store($user); + $this->context->transport->success = true; + } + + + + + /** + * @route /user/{name}/delete + * @validate name [^\/]+ + */ + public function deleteAction($name) + { + $user = self::locateUser($name); + $secondary = $user->id == $this->context->user->id ? 'own' : 'all'; + PrivilegesHelper::confirmWithException($this->context->user, Privilege::ViewUser, $secondary); + PrivilegesHelper::confirmWithException($this->context->user, Privilege::DeleteUser, $secondary); + + $this->context->handleExceptions = true; + $this->context->transport->user = $user; + $this->context->transport->tab = 'delete'; + $this->context->viewName = 'user-view'; + $this->context->stylesheets []= 'user-view.css'; + $this->context->subTitle = $name; + + $this->context->suppliedOldPassword = $suppliedOldPassword = InputHelper::get('old-password'); + + if (InputHelper::get('remove')) + { + if ($this->context->user->id == $user->id) + { + $suppliedPasswordHash = Model_User::hashPassword($suppliedOldPassword, $user->pass_salt); + if ($suppliedPasswordHash != $user->pass_hash) + throw new SimpleException('Must supply valid password'); + } + $user->ownFavoritee = []; + R::store($user); + #R::trashAll(R::findAll('favoritee', 'user_id = ?', [$user->id])); + R::trash($user); + \Chibi\UrlHelper::forward(\Chibi\UrlHelper::route('index', 'index')); + $this->context->transport->success = true; + } + } + + + /** * @route /user/{name}/edit * @validate name [^\/]+ */ public function editAction($name) { + $user = self::locateUser($name); + $edited = false; + $secondary = $user->id == $this->context->user->id ? 'own' : 'all'; + PrivilegesHelper::confirmWithException($this->context->user, Privilege::ViewUser, $secondary); + + $this->context->handleExceptions = true; + $this->context->transport->user = $user; + $this->context->transport->tab = 'edit'; $this->context->viewName = 'user-view'; $this->context->stylesheets []= 'user-view.css'; $this->context->subTitle = $name; - PrivilegesHelper::confirmWithException($this->context->user, Privilege::ViewUser); - $user = self::locateUser($name); - $this->context->transport->user = $user; - $this->context->transport->tab = 'edit'; + $this->context->suppliedOldPassword = $suppliedOldPassword = InputHelper::get('old-password'); + $this->context->suppliedName = $suppliedName = InputHelper::get('name'); + $this->context->suppliedPassword1 = $suppliedPassword1 = InputHelper::get('password1'); + $this->context->suppliedPassword2 = $suppliedPassword2 = InputHelper::get('password2'); + $this->context->suppliedEmail = $suppliedEmail = InputHelper::get('email'); + $this->context->suppliedAccessRank = $suppliedAccessRank = InputHelper::get('access-rank'); + $oldPasswordHash = $user->pass_hash; + + if ($suppliedName != '' and $suppliedName != $user->name) + { + PrivilegesHelper::confirmWithException($this->context->user, Privilege::ChangeUserName, $secondary); + $suppliedName = Model_User::validateUserName($suppliedName); + $user->name = $suppliedName; + $edited = true; + } + + if ($suppliedPassword1 != '') + { + PrivilegesHelper::confirmWithException($this->context->user, Privilege::ChangeUserPassword, $secondary); + if ($suppliedPassword1 != $suppliedPassword2) + throw new SimpleException('Specified passwords must be the same'); + $suppliedPassword = Model_User::validatePassword($suppliedPassword1); + $user->pass_hash = Model_User::hashPassword($suppliedPassword, $user->pass_salt); + $edited = true; + } + + if ($suppliedEmail != '' and $suppliedEmail != $user->email) + { + PrivilegesHelper::confirmWithException($this->context->user, Privilege::ChangeUserEmail, $secondary); + $suppliedEmail = Model_User::validateEmail($suppliedEmail); + $user->email = $suppliedEmail; + $edited = true; + } + + if ($suppliedAccessRank != '' and $suppliedAccessRank != $user->access_rank) + { + PrivilegesHelper::confirmWithException($this->context->user, Privilege::ChangeUserAccessRank, $secondary); + $suppliedAccessRank = Model_User::validateAccessRank($suppliedAccessRank); + $user->access_rank = $suppliedAccessRank; + $edited = true; + } + + if ($edited) + { + if ($this->context->user->id == $user->id) + { + $suppliedPasswordHash = Model_User::hashPassword($suppliedOldPassword, $user->pass_salt); + if ($suppliedPasswordHash != $oldPasswordHash) + throw new SimpleException('Must supply valid old password'); + } + R::store($user); + $this->context->transport->success = true; + } } @@ -49,14 +189,6 @@ class UserController */ public function viewAction($name, $tab, $page) { - $this->context->stylesheets []= 'user-view.css'; - $this->context->stylesheets []= 'post-list.css'; - $this->context->stylesheets []= 'paginator.css'; - if ($this->config->browsing->endlessScrolling) - $this->context->scripts []= 'paginator-endless.js'; - $this->context->subTitle = $name; - PrivilegesHelper::confirmWithException($this->context->user, Privilege::ViewUser); - $postsPerPage = intval($this->config->browsing->postsPerPage); $user = self::locateUser($name); if ($tab === null) @@ -64,6 +196,15 @@ class UserController if ($page === null) $page = 1; + $secondary = $user->id == $this->context->user->id ? 'own' : 'all'; + PrivilegesHelper::confirmWithException($this->context->user, Privilege::ViewUser, $secondary); + $this->context->stylesheets []= 'user-view.css'; + $this->context->stylesheets []= 'post-list.css'; + $this->context->stylesheets []= 'paginator.css'; + if ($this->config->browsing->endlessScrolling) + $this->context->scripts []= 'paginator-endless.js'; + $this->context->subTitle = $name; + $buildDbQuery = function($dbQuery, $user, $tab) { $dbQuery->from('post'); diff --git a/src/Models/Model_User.php b/src/Models/Model_User.php index 4d4e2039..91993ac9 100644 --- a/src/Models/Model_User.php +++ b/src/Models/Model_User.php @@ -51,4 +51,73 @@ class Model_User extends RedBean_SimpleModel $this->setSetting('safety-' . $safety, true); } } + + public static function validateUserName($userName) + { + $userName = trim($userName); + + $dbUser = R::findOne('user', 'name = ?', [$userName]); + if ($dbUser !== null) + { + if (!$dbUser->email_confirmed and \Chibi\Registry::getConfig()->registration->emailActivation) + throw new SimpleException('User with this name is already registered and awaits e-mail confirmation'); + + if (!$dbUser->staff_confirmed and \Chibi\Registry::getConfig()->registration->staffActivation) + throw new SimpleException('User with this name is already registered and awaits staff confirmation'); + + throw new SimpleException('User with this name is already registered'); + } + + $userNameMinLength = intval(\Chibi\Registry::getConfig()->registration->userNameMinLength); + $userNameRegex = \Chibi\Registry::getConfig()->registration->userNameRegex; + + if (strlen($userName) < $userNameMinLength) + throw new SimpleException(sprintf('User name must have at least %d characters', $userNameMinLength)); + + if (!preg_match($userNameRegex, $userName)) + throw new SimpleException('User name contains invalid characters'); + + return $userName; + } + + public static function validatePassword($password) + { + $passMinLength = intval(\Chibi\Registry::getConfig()->registration->passMinLength); + $passRegex = \Chibi\Registry::getConfig()->registration->passRegex; + + if (strlen($password) < $passMinLength) + throw new SimpleException(sprintf('Password must have at least %d characters', $passMinLength)); + + if (!preg_match($passRegex, $password)) + throw new SimpleException('Password contains invalid characters'); + + return $password; + } + + public static function validateEmail($email) + { + $email = trim($email); + + if (!empty($email) and !TextHelper::isValidEmail($email)) + throw new SimpleException('E-mail address appears to be invalid'); + + return $email; + } + + public static function validateAccessRank($accessRank) + { + $accessRank = intval($accessRank); + + if (!in_array($accessRank, AccessRank::getAll())) + throw new SimpleException('Invalid access rank type "' . $accessRank . '"'); + + return $accessRank; + } + + public static function hashPassword($pass, $salt2) + { + $salt1 = \Chibi\Registry::getConfig()->registration->salt; + return sha1($salt1 . $salt2 . $pass); + } + } diff --git a/src/Models/Privilege.php b/src/Models/Privilege.php index b8a5888a..3c21b205 100644 --- a/src/Models/Privilege.php +++ b/src/Models/Privilege.php @@ -20,7 +20,8 @@ class Privilege extends Enum const ChangeUserAccessRank = 16; const ChangeUserEmail = 17; const ChangeUserName = 18; + const DeleteUser = 19; - const ListComments = 19; - const ListTags = 20; + const ListComments = 20; + const ListTags = 21; } diff --git a/src/Views/auth-login.phtml b/src/Views/auth-login.phtml index 0016dbf3..06b8f948 100644 --- a/src/Views/auth-login.phtml +++ b/src/Views/auth-login.phtml @@ -5,13 +5,13 @@
- - + +
- - + +
context->transport->errorMessage)): ?> diff --git a/src/Views/auth-register.phtml b/src/Views/auth-register.phtml index c2393d5c..7c7d1767 100644 --- a/src/Views/auth-register.phtml +++ b/src/Views/auth-register.phtml @@ -15,18 +15,18 @@
- - + +
- - + +
- - + +
diff --git a/src/Views/post-view.phtml b/src/Views/post-view.phtml index f033e4f0..dcf472d0 100644 --- a/src/Views/post-view.phtml +++ b/src/Views/post-view.phtml @@ -44,31 +44,43 @@