summaryrefslogtreecommitdiff
path: root/modules/user
diff options
context:
space:
mode:
authorTristan Zur <tzur@webserver.ccwn.org>2015-06-10 20:55:53 +0200
committerTristan Zur <tzur@webserver.ccwn.org>2015-06-10 20:55:53 +0200
commit406abd7c4df1ace2cd3e4e17159e8941a2e8c0c4 (patch)
treea324be16021f44f2fd6d55e609f47024e945b1db /modules/user
Initial import
Diffstat (limited to 'modules/user')
-rw-r--r--modules/user/config/identity.php37
-rw-r--r--modules/user/controllers/admin_users.php442
-rw-r--r--modules/user/controllers/password.php141
-rw-r--r--modules/user/controllers/users.php239
-rw-r--r--modules/user/css/user.css120
-rw-r--r--modules/user/helpers/group.php82
-rw-r--r--modules/user/helpers/user.php176
-rw-r--r--modules/user/helpers/user_event.php30
-rw-r--r--modules/user/helpers/user_installer.php142
-rw-r--r--modules/user/helpers/user_theme.php30
-rw-r--r--modules/user/images/progressImg1.pngbin0 -> 390 bytes
-rw-r--r--modules/user/js/password_strength.js39
-rw-r--r--modules/user/lib/PasswordHash.php248
-rw-r--r--modules/user/libraries/drivers/IdentityProvider/Gallery.php164
-rw-r--r--modules/user/models/group.php89
-rw-r--r--modules/user/models/user.php185
-rw-r--r--modules/user/module.info8
-rw-r--r--modules/user/views/admin_users.html.php142
-rw-r--r--modules/user/views/admin_users_delete_user.html.php7
-rw-r--r--modules/user/views/admin_users_group.html.php40
-rw-r--r--modules/user/views/reset_password.html.php18
21 files changed, 2379 insertions, 0 deletions
diff --git a/modules/user/config/identity.php b/modules/user/config/identity.php
new file mode 100644
index 0000000..f2fcebf
--- /dev/null
+++ b/modules/user/config/identity.php
@@ -0,0 +1,37 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+/*
+ * @package Identity
+ *
+ * User settings, defined as arrays, or "groups". If no group name is
+ * used when loading the cache library, the group named "default" will be used.
+ *
+ * Each group can be used independently, and multiple groups can be used at once.
+ *
+ * Group Options:
+ * driver - User backend driver. Gallery comes with Gallery user driver.
+ * allow_updates - Flag to indicate that the back end allows updates.
+ * params - Driver parameters, specific to each driver.
+ */
+$config["user"] = array (
+ "driver" => "gallery",
+ "allow_updates" => true,
+ "params" => array(),
+);
diff --git a/modules/user/controllers/admin_users.php b/modules/user/controllers/admin_users.php
new file mode 100644
index 0000000..ed589a3
--- /dev/null
+++ b/modules/user/controllers/admin_users.php
@@ -0,0 +1,442 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class Admin_Users_Controller extends Admin_Controller {
+ public function index() {
+ $view = new Admin_View("admin.html");
+ $view->page_title = t("Users and groups");
+ $view->page_type = "collection";
+ $view->page_subtype = "admin_users";
+ $view->content = new View("admin_users.html");
+
+ // @todo: add this as a config option
+ $page_size = module::get_var("user", "page_size", 10);
+ $page = Input::instance()->get("page", "1");
+ $builder = db::build();
+ $user_count = $builder->from("users")->count_records();
+
+ // Pagination info
+ $view->page = $page;
+ $view->page_size = $page_size;
+ $view->children_count = $user_count;
+ $view->max_pages = ceil($view->children_count / $view->page_size);
+
+ $view->content->pager = new Pagination();
+ $view->content->pager->initialize(
+ array("query_string" => "page",
+ "total_items" => $user_count,
+ "items_per_page" => $page_size,
+ "style" => "classic"));
+
+ // Make sure that the page references a valid offset
+ if ($page < 1) {
+ url::redirect(url::merge(array("page" => 1)));
+ } else if ($page > $view->content->pager->total_pages) {
+ url::redirect(url::merge(array("page" => $view->content->pager->total_pages)));
+ }
+
+ // Join our users against the items table so that we can get a count of their items
+ // in the same query.
+ $view->content->users = ORM::factory("user")
+ ->order_by("users.name", "ASC")
+ ->find_all($page_size, $view->content->pager->sql_offset);
+ $view->content->groups = ORM::factory("group")->order_by("name", "ASC")->find_all();
+
+ print $view;
+ }
+
+ public function add_user() {
+ access::verify_csrf();
+
+ $form = $this->_get_user_add_form_admin();
+ try {
+ $user = ORM::factory("user");
+ $valid = $form->validate();
+ $user->name = $form->add_user->inputs["name"]->value;
+ $user->full_name = $form->add_user->full_name->value;
+ $user->password = $form->add_user->password->value;
+ $user->email = $form->add_user->email->value;
+ $user->url = $form->add_user->url->value;
+ $user->locale = $form->add_user->locale->value;
+ $user->admin = $form->add_user->admin->checked;
+ $user->validate();
+ } catch (ORM_Validation_Exception $e) {
+ // Translate ORM validation errors into form error messages
+ foreach ($e->validation->errors() as $key => $error) {
+ $form->add_user->inputs[$key]->add_error($error, 1);
+ }
+ $valid = false;
+ }
+
+ if ($valid) {
+ $user->save();
+ module::event("user_add_form_admin_completed", $user, $form);
+ message::success(t("Created user %user_name", array("user_name" => $user->name)));
+ json::reply(array("result" => "success"));
+ } else {
+ print json::reply(array("result" => "error", "html" => (string)$form));
+ }
+ }
+
+ public function add_user_form() {
+ print $this->_get_user_add_form_admin();
+ }
+
+ public function delete_user($id) {
+ access::verify_csrf();
+
+ if ($id == identity::active_user()->id || $id == user::guest()->id) {
+ access::forbidden();
+ }
+
+ $user = user::lookup($id);
+ if (empty($user)) {
+ throw new Kohana_404_Exception();
+ }
+
+ $form = $this->_get_user_delete_form_admin($user);
+ if($form->validate()) {
+ $name = $user->name;
+ $user->delete();
+ } else {
+ json::reply(array("result" => "error", "html" => (string)$form));
+ }
+
+ $message = t("Deleted user %user_name", array("user_name" => $name));
+ log::success("user", $message);
+ message::success($message);
+ json::reply(array("result" => "success"));
+ }
+
+ public function delete_user_form($id) {
+ $user = user::lookup($id);
+ if (empty($user)) {
+ throw new Kohana_404_Exception();
+ }
+ $v = new View("admin_users_delete_user.html");
+ $v->user = $user;
+ $v->form = $this->_get_user_delete_form_admin($user);
+ print $v;
+ }
+
+ public function edit_user($id) {
+ access::verify_csrf();
+
+ $user = user::lookup($id);
+ if (empty($user)) {
+ throw new Kohana_404_Exception();
+ }
+
+ $form = $this->_get_user_edit_form_admin($user);
+ try {
+ $valid = $form->validate();
+ $user->name = $form->edit_user->inputs["name"]->value;
+ $user->full_name = $form->edit_user->full_name->value;
+ if ($form->edit_user->password->value) {
+ $user->password = $form->edit_user->password->value;
+ }
+ $user->email = $form->edit_user->email->value;
+ $user->url = $form->edit_user->url->value;
+ $user->locale = $form->edit_user->locale->value;
+ if ($user->id != identity::active_user()->id) {
+ $user->admin = $form->edit_user->admin->checked;
+ }
+
+ $user->validate();
+ } catch (ORM_Validation_Exception $e) {
+ // Translate ORM validation errors into form error messages
+ foreach ($e->validation->errors() as $key => $error) {
+ $form->edit_user->inputs[$key]->add_error($error, 1);
+ }
+ $valid = false;
+ }
+
+ if ($valid) {
+ $user->save();
+ module::event("user_edit_form_admin_completed", $user, $form);
+ message::success(t("Changed user %user_name", array("user_name" => $user->name)));
+ json::reply(array("result" => "success"));
+ } else {
+ json::reply(array("result" => "error", "html" => (string) $form));
+ }
+ }
+
+ public function edit_user_form($id) {
+ $user = user::lookup($id);
+ if (empty($user)) {
+ throw new Kohana_404_Exception();
+ }
+
+ print $this->_get_user_edit_form_admin($user);
+ }
+
+ public function add_user_to_group($user_id, $group_id) {
+ access::verify_csrf();
+ $group = group::lookup($group_id);
+ $user = user::lookup($user_id);
+ $group->add($user);
+ $group->save();
+ }
+
+ public function remove_user_from_group($user_id, $group_id) {
+ access::verify_csrf();
+ $group = group::lookup($group_id);
+ $user = user::lookup($user_id);
+ $group->remove($user);
+ $group->save();
+ }
+
+ public function group($group_id) {
+ $view = new View("admin_users_group.html");
+ $view->group = group::lookup($group_id);
+ print $view;
+ }
+
+ public function add_group() {
+ access::verify_csrf();
+
+ $form = $this->_get_group_add_form_admin();
+ try {
+ $valid = $form->validate();
+ $group = ORM::factory("group");
+ $group->name = $form->add_group->inputs["name"]->value;
+ $group->validate();
+ } catch (ORM_Validation_Exception $e) {
+ // Translate ORM validation errors into form error messages
+ foreach ($e->validation->errors() as $key => $error) {
+ $form->add_group->inputs[$key]->add_error($error, 1);
+ }
+ $valid = false;
+ }
+
+ if ($valid) {
+ $group->save();
+ message::success(
+ t("Created group %group_name", array("group_name" => $group->name)));
+ json::reply(array("result" => "success"));
+ } else {
+ json::reply(array("result" => "error", "html" => (string)$form));
+ }
+ }
+
+ public function add_group_form() {
+ print $this->_get_group_add_form_admin();
+ }
+
+ public function delete_group($id) {
+ access::verify_csrf();
+
+ $group = group::lookup($id);
+ if (empty($group)) {
+ throw new Kohana_404_Exception();
+ }
+
+ $form = $this->_get_group_delete_form_admin($group);
+ if ($form->validate()) {
+ $name = $group->name;
+ $group->delete();
+ } else {
+ json::reply(array("result" => "error", "html" => (string) $form));
+ }
+
+ $message = t("Deleted group %group_name", array("group_name" => $name));
+ log::success("group", $message);
+ message::success($message);
+ json::reply(array("result" => "success"));
+ }
+
+ public function delete_group_form($id) {
+ $group = group::lookup($id);
+ if (empty($group)) {
+ throw new Kohana_404_Exception();
+ }
+
+ print $this->_get_group_delete_form_admin($group);
+ }
+
+ public function edit_group($id) {
+ access::verify_csrf();
+
+ $group = group::lookup($id);
+ if (empty($group)) {
+ throw new Kohana_404_Exception();
+ }
+
+ $form = $this->_get_group_edit_form_admin($group);
+ try {
+ $valid = $form->validate();
+ $group->name = $form->edit_group->inputs["name"]->value;
+ $group->validate();
+ } catch (ORM_Validation_Exception $e) {
+ // Translate ORM validation errors into form error messages
+ foreach ($e->validation->errors() as $key => $error) {
+ $form->edit_group->inputs[$key]->add_error($error, 1);
+ }
+ $valid = false;
+ }
+
+ if ($valid) {
+ $group->save();
+ message::success(
+ t("Changed group %group_name", array("group_name" => $group->name)));
+ json::reply(array("result" => "success"));
+ } else {
+ $group->reload();
+ message::error(
+ t("Failed to change group %group_name", array("group_name" => $group->name)));
+ json::reply(array("result" => "error", "html" => (string) $form));
+ }
+ }
+
+ public function edit_group_form($id) {
+ $group = group::lookup($id);
+ if (empty($group)) {
+ throw new Kohana_404_Exception();
+ }
+
+ print $this->_get_group_edit_form_admin($group);
+ }
+
+ /* User Form Definitions */
+ static function _get_user_edit_form_admin($user) {
+ $form = new Forge(
+ "admin/users/edit_user/$user->id", "", "post", array("id" => "g-edit-user-form"));
+ $group = $form->group("edit_user")->label(t("Edit user"));
+ $group->input("name")->label(t("Username"))->id("g-username")->value($user->name)
+ ->error_messages("required", t("A name is required"))
+ ->error_messages("conflict", t("There is already a user with that username"))
+ ->error_messages("length", t("This name is too long"));
+ $group->input("full_name")->label(t("Full name"))->id("g-fullname")->value($user->full_name)
+ ->error_messages("length", t("This name is too long"));
+ $group->password("password")->label(t("Password"))->id("g-password")
+ ->error_messages("min_length", t("This password is too short"));
+ $group->script("")
+ ->text(
+ '$("form").ready(function(){$(\'input[name="password"]\').user_password_strength();});');
+ $group->password("password2")->label(t("Confirm password"))->id("g-password2")
+ ->error_messages("matches", t("The passwords you entered do not match"))
+ ->matches($group->password);
+ $group->input("email")->label(t("Email"))->id("g-email")->value($user->email)
+ ->error_messages("required", t("You must enter a valid email address"))
+ ->error_messages("length", t("This email address is too long"))
+ ->error_messages("email", t("You must enter a valid email address"));
+ $group->input("url")->label(t("URL"))->id("g-url")->value($user->url)
+ ->error_messages("url", t("You must enter a valid URL"));
+ self::_add_locale_dropdown($group, $user);
+ $group->checkbox("admin")->label(t("Admin"))->id("g-admin")->checked($user->admin);
+
+ // Don't allow the user to control their own admin bit, else you can lock yourself out
+ if ($user->id == identity::active_user()->id) {
+ $group->admin->disabled(1);
+ }
+
+ module::event("user_edit_form_admin", $user, $form);
+ $group->submit("")->value(t("Modify user"));
+ return $form;
+ }
+
+ static function _get_user_add_form_admin() {
+ $form = new Forge("admin/users/add_user", "", "post", array("id" => "g-add-user-form"));
+ $group = $form->group("add_user")->label(t("Add user"));
+ $group->input("name")->label(t("Username"))->id("g-username")
+ ->error_messages("required", t("A name is required"))
+ ->error_messages("length", t("This name is too long"))
+ ->error_messages("conflict", t("There is already a user with that username"));
+ $group->input("full_name")->label(t("Full name"))->id("g-fullname")
+ ->error_messages("length", t("This name is too long"));
+ $group->password("password")->label(t("Password"))->id("g-password")
+ ->error_messages("min_length", t("This password is too short"));
+ $group->script("")
+ ->text(
+ '$("form").ready(function(){$(\'input[name="password"]\').user_password_strength();});');
+ $group->password("password2")->label(t("Confirm password"))->id("g-password2")
+ ->error_messages("matches", t("The passwords you entered do not match"))
+ ->matches($group->password);
+ $group->input("email")->label(t("Email"))->id("g-email")
+ ->error_messages("required", t("You must enter a valid email address"))
+ ->error_messages("length", t("This email address is too long"))
+ ->error_messages("email", t("You must enter a valid email address"));
+ $group->input("url")->label(t("URL"))->id("g-url")
+ ->error_messages("url", t("You must enter a valid URL"));
+ self::_add_locale_dropdown($group);
+ $group->checkbox("admin")->label(t("Admin"))->id("g-admin");
+
+ module::event("user_add_form_admin", $user, $form);
+ $group->submit("")->value(t("Add user"));
+ return $form;
+ }
+
+ private static function _add_locale_dropdown(&$form, $user=null) {
+ $locales = locales::installed();
+ foreach ($locales as $locale => $display_name) {
+ $locales[$locale] = SafeString::of_safe_html($display_name);
+ }
+
+ // Put "none" at the first position in the array
+ $locales = array_merge(array("" => t("« none »")), $locales);
+ $selected_locale = ($user && $user->locale) ? $user->locale : "";
+ $form->dropdown("locale")
+ ->label(t("Language preference"))
+ ->options($locales)
+ ->selected($selected_locale);
+ }
+
+ private function _get_user_delete_form_admin($user) {
+ $form = new Forge("admin/users/delete_user/$user->id", "", "post",
+ array("id" => "g-delete-user-form"));
+ $group = $form->group("delete_user")->label(
+ t("Delete user %name?", array("name" => $user->display_name())));
+ $group->submit("")->value(t("Delete"));
+ return $form;
+ }
+
+ /* Group Form Definitions */
+ private function _get_group_edit_form_admin($group) {
+ $form = new Forge("admin/users/edit_group/$group->id", "", "post", array("id" => "g-edit-group-form"));
+ $form_group = $form->group("edit_group")->label(t("Edit group"));
+ $form_group->input("name")->label(t("Name"))->id("g-name")->value($group->name)
+ ->error_messages("required", t("A name is required"));
+ $form_group->inputs["name"]->error_messages("conflict", t("There is already a group with that name"))
+ ->error_messages("required", t("You must enter a group name"))
+ ->error_messages("length",
+ t("The group name must be less than %max_length characters",
+ array("max_length" => 255)));
+ $form_group->submit("")->value(t("Save"));
+ return $form;
+ }
+
+ private function _get_group_add_form_admin() {
+ $form = new Forge("admin/users/add_group", "", "post", array("id" => "g-add-group-form"));
+ $form_group = $form->group("add_group")->label(t("Add group"));
+ $form_group->input("name")->label(t("Name"))->id("g-name");
+ $form_group->inputs["name"]->error_messages("conflict", t("There is already a group with that name"))
+ ->error_messages("required", t("You must enter a group name"));
+ $form_group->submit("")->value(t("Add group"));
+ return $form;
+ }
+
+ private function _get_group_delete_form_admin($group) {
+ $form = new Forge("admin/users/delete_group/$group->id", "", "post",
+ array("id" => "g-delete-group-form"));
+ $form_group = $form->group("delete_group")->label(
+ t("Are you sure you want to delete group %group_name?", array("group_name" => $group->name)));
+ $form_group->submit("")->value(t("Delete"));
+ return $form;
+ }
+}
diff --git a/modules/user/controllers/password.php b/modules/user/controllers/password.php
new file mode 100644
index 0000000..ea14144
--- /dev/null
+++ b/modules/user/controllers/password.php
@@ -0,0 +1,141 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class Password_Controller extends Controller {
+ const ALLOW_MAINTENANCE_MODE = true;
+ const ALLOW_PRIVATE_GALLERY = true;
+
+ public function reset() {
+ $form = self::_reset_form();
+ if (request::method() == "post") {
+ // @todo separate the post from get parts of this function
+ access::verify_csrf();
+ // Basic validation (was some user name specified?)
+ if ($form->validate()) {
+ $this->_send_reset($form);
+ } else {
+ json::reply(array("result" => "error", "html" => (string)$form));
+ }
+ } else {
+ print $form;
+ }
+ }
+
+ public function do_reset() {
+ if (request::method() == "post") {
+ $this->_change_password();
+ } else {
+ $user = user::lookup_by_hash(Input::instance()->get("key"));
+ if (!empty($user)) {
+ print $this->_new_password_form($user->hash);
+ } else {
+ throw new Exception("@todo FORBIDDEN", 503);
+ }
+ }
+ }
+
+ private function _send_reset($form) {
+ $user_name = $form->reset->inputs["name"]->value;
+ $user = user::lookup_by_name($user_name);
+ if ($user && !empty($user->email)) {
+ $user->hash = random::hash();
+ $user->save();
+ $message = new View("reset_password.html");
+ $message->confirm_url = url::abs_site("password/do_reset?key=$user->hash");
+ $message->user = $user;
+
+ Sendmail::factory()
+ ->to($user->email)
+ ->subject(t("Password Reset Request"))
+ ->header("Mime-Version", "1.0")
+ ->header("Content-type", "text/html; charset=UTF-8")
+ ->message($message->render())
+ ->send();
+
+ log::success(
+ "user",
+ t("Password reset email sent for user %name", array("name" => $user->name)));
+ } else if (!$user) {
+ // Don't include the username here until you're sure that it's XSS safe
+ log::warning(
+ "user", t("Password reset email requested for user %user_name, which does not exist.",
+ array("user_name" => $user_name)));
+ } else {
+ log::warning(
+ "user", t("Password reset failed for %user_name (has no email address on record).",
+ array("user_name" => $user->name)));
+ }
+
+ // Always pretend that an email has been sent to avoid leaking
+ // information on what user names are actually real.
+ message::success(t("Password reset email sent"));
+ json::reply(array("result" => "success"));
+ }
+
+ private static function _reset_form() {
+ $form = new Forge(url::current(true), "", "post", array("id" => "g-reset-form"));
+ $group = $form->group("reset")->label(t("Reset Password"));
+ $group->input("name")->label(t("Username"))->id("g-name")->class(null)
+ ->rules("required")
+ ->error_messages("required", t("You must enter a user name"));
+ $group->submit("")->value(t("Reset"));
+
+ return $form;
+ }
+
+ private function _new_password_form($hash=null) {
+ $template = new Theme_View("page.html", "other", "reset");
+
+ $form = new Forge("password/do_reset", "", "post", array("id" => "g-change-password-form"));
+ $group = $form->group("reset")->label(t("Change Password"));
+ $hidden = $group->hidden("hash");
+ if (!empty($hash)) {
+ $hidden->value($hash);
+ }
+ $minimum_length = module::get_var("user", "minimum_password_length", 5);
+ $input_password = $group->password("password")->label(t("Password"))->id("g-password")
+ ->rules($minimum_length ? "required|length[$minimum_length, 40]" : "length[40]");
+ $group->password("password2")->label(t("Confirm Password"))->id("g-password2")
+ ->matches($group->password);
+ $group->inputs["password2"]->error_messages(
+ "mistyped", t("The password and the confirm password must match"));
+ $group->submit("")->value(t("Update"));
+
+ $template->content = $form;
+ return $template;
+ }
+
+ private function _change_password() {
+ $view = $this->_new_password_form();
+ if ($view->content->validate()) {
+ $user = user::lookup_by_hash(Input::instance()->post("hash"));
+ if (empty($user)) {
+ throw new Exception("@todo FORBIDDEN", 503);
+ }
+
+ $user->password = $view->content->reset->password->value;
+ $user->hash = null;
+ $user->save();
+ message::success(t("Password reset successfully"));
+ url::redirect(item::root()->abs_url());
+ } else {
+ print $view;
+ }
+ }
+} \ No newline at end of file
diff --git a/modules/user/controllers/users.php b/modules/user/controllers/users.php
new file mode 100644
index 0000000..ee81344
--- /dev/null
+++ b/modules/user/controllers/users.php
@@ -0,0 +1,239 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class Users_Controller extends Controller {
+ public function update($id) {
+ $user = user::lookup($id);
+ if (!$user || $user->guest || $user->id != identity::active_user()->id) {
+ access::forbidden();
+ }
+
+ $form = $this->_get_edit_form($user);
+ try {
+ $valid = $form->validate();
+ $user->full_name = $form->edit_user->full_name->value;
+ $user->url = $form->edit_user->url->value;
+
+ if (count(locales::installed()) > 1 &&
+ $user->locale != $form->edit_user->locale->value) {
+ $user->locale = $form->edit_user->locale->value;
+ $flush_locale_cookie = true;
+ }
+
+ $user->validate();
+ } catch (ORM_Validation_Exception $e) {
+ // Translate ORM validation errors into form error messages
+ foreach ($e->validation->errors() as $key => $error) {
+ $form->edit_user->inputs[$key]->add_error($error, 1);
+ }
+ $valid = false;
+ }
+
+ if ($valid) {
+ if (isset($flush_locale_cookie)) {
+ // Delete the session based locale preference
+ setcookie("g_locale", "", time() - 24 * 3600, "/");
+ }
+
+ $user->save();
+ module::event("user_edit_form_completed", $user, $form);
+ message::success(t("User information updated"));
+ json::reply(array("result" => "success",
+ "resource" => url::site("users/{$user->id}")));
+ } else {
+ json::reply(array("result" => "error", "html" => (string)$form));
+ }
+ }
+
+ public function change_password($id) {
+ $user = user::lookup($id);
+ if (!$user || $user->guest || $user->id != identity::active_user()->id) {
+ access::forbidden();
+ }
+
+ $form = $this->_get_change_password_form($user);
+ try {
+ $valid = $form->validate();
+ $user->password = $form->change_password->password->value;
+ $user->validate();
+ } catch (ORM_Validation_Exception $e) {
+ // Translate ORM validation errors into form error messages
+ foreach ($e->validation->errors() as $key => $error) {
+ $form->change_password->inputs[$key]->add_error($error, 1);
+ }
+ $valid = false;
+ }
+
+ if ($valid) {
+ $user->save();
+ module::event("user_change_password_form_completed", $user, $form);
+ message::success(t("Password changed"));
+ module::event("user_auth", $user);
+ module::event("user_password_change", $user);
+ json::reply(array("result" => "success",
+ "resource" => url::site("users/{$user->id}")));
+ } else {
+ log::warning("user", t("Failed password change for %name", array("name" => $user->name)));
+ $name = $user->name;
+ module::event("user_auth_failed", $name);
+ json::reply(array("result" => "error", "html" => (string)$form));
+ }
+ }
+
+ public function change_email($id) {
+ $user = user::lookup($id);
+ if (!$user || $user->guest || $user->id != identity::active_user()->id) {
+ access::forbidden();
+ }
+
+ $form = $this->_get_change_email_form($user);
+ try {
+ $valid = $form->validate();
+ $user->email = $form->change_email->email->value;
+ $user->validate();
+ } catch (ORM_Validation_Exception $e) {
+ // Translate ORM validation errors into form error messages
+ foreach ($e->validation->errors() as $key => $error) {
+ $form->change_email->inputs[$key]->add_error($error, 1);
+ }
+ $valid = false;
+ }
+
+ if ($valid) {
+ $user->save();
+ module::event("user_change_email_form_completed", $user, $form);
+ message::success(t("Email address changed"));
+ module::event("user_auth", $user);
+ json::reply(array("result" => "success",
+ "resource" => url::site("users/{$user->id}")));
+ } else {
+ log::warning("user", t("Failed email change for %name", array("name" => $user->name)));
+ $name = $user->name;
+ module::event("user_auth_failed", $name);
+ json::reply(array("result" => "error", "html" => (string)$form));
+ }
+ }
+
+ public function form_edit($id) {
+ $user = user::lookup($id);
+ if (!$user || $user->guest || $user->id != identity::active_user()->id) {
+ access::forbidden();
+ }
+
+ print $this->_get_edit_form($user);
+ }
+
+ public function form_change_password($id) {
+ $user = user::lookup($id);
+ if (!$user || $user->guest || $user->id != identity::active_user()->id) {
+ access::forbidden();
+ }
+
+ print $this->_get_change_password_form($user);
+ }
+
+ public function form_change_email($id) {
+ $user = user::lookup($id);
+ if (!$user || $user->guest || $user->id != identity::active_user()->id) {
+ access::forbidden();
+ }
+
+ print $this->_get_change_email_form($user);
+ }
+
+ private function _get_change_password_form($user) {
+ $form = new Forge(
+ "users/change_password/$user->id", "", "post", array("id" => "g-change-password-user-form"));
+ $group = $form->group("change_password")->label(t("Change your password"));
+ $group->password("old_password")->label(t("Old password"))->id("g-password")
+ ->callback("auth::validate_too_many_failed_auth_attempts")
+ ->callback("user::valid_password")
+ ->error_messages("invalid_password", t("Incorrect password"))
+ ->error_messages(
+ "too_many_failed_auth_attempts",
+ t("Too many incorrect passwords. Try again later"));
+ $group->password("password")->label(t("New password"))->id("g-password")
+ ->error_messages("min_length", t("Your new password is too short"));
+ $group->script("")
+ ->text(
+ '$("form").ready(function(){$(\'input[name="password"]\').user_password_strength();});');
+ $group->password("password2")->label(t("Confirm new password"))->id("g-password2")
+ ->matches($group->password)
+ ->error_messages("matches", t("The passwords you entered do not match"));
+
+ module::event("user_change_password_form", $user, $form);
+ $group->submit("")->value(t("Save"));
+ return $form;
+ }
+
+ private function _get_change_email_form($user) {
+ $form = new Forge(
+ "users/change_email/$user->id", "", "post", array("id" => "g-change-email-user-form"));
+ $group = $form->group("change_email")->label(t("Change your email address"));
+ $group->password("password")->label(t("Current password"))->id("g-password")
+ ->callback("auth::validate_too_many_failed_auth_attempts")
+ ->callback("user::valid_password")
+ ->error_messages("invalid_password", t("Incorrect password"))
+ ->error_messages(
+ "too_many_failed_auth_attempts",
+ t("Too many incorrect passwords. Try again later"));
+ $group->input("email")->label(t("New email address"))->id("g-email")->value($user->email)
+ ->error_messages("email", t("You must enter a valid email address"))
+ ->error_messages("length", t("Your email address is too long"))
+ ->error_messages("required", t("You must enter a valid email address"));
+
+ module::event("user_change_email_form", $user, $form);
+ $group->submit("")->value(t("Save"));
+ return $form;
+ }
+
+ private function _get_edit_form($user) {
+ $form = new Forge("users/update/$user->id", "", "post", array("id" => "g-edit-user-form"));
+ $group = $form->group("edit_user")->label(t("Edit your profile"));
+ $group->input("full_name")->label(t("Full Name"))->id("g-fullname")->value($user->full_name)
+ ->error_messages("length", t("Your name is too long"));
+ self::_add_locale_dropdown($group, $user);
+ $group->input("url")->label(t("URL"))->id("g-url")->value($user->url)
+ ->error_messages("url", t("You must enter a valid url"));
+
+ module::event("user_edit_form", $user, $form);
+ $group->submit("")->value(t("Save"));
+ return $form;
+ }
+
+ /** @todo combine with Admin_Users_Controller::_add_locale_dropdown */
+ private function _add_locale_dropdown(&$form, $user=null) {
+ $locales = locales::installed();
+ if (count($locales) <= 1) {
+ return;
+ }
+
+ foreach ($locales as $locale => $display_name) {
+ $locales[$locale] = SafeString::of_safe_html($display_name);
+ }
+
+ // Put "none" at the first position in the array
+ $locales = array_merge(array("" => t("« none »")), $locales);
+ $selected_locale = ($user && $user->locale) ? $user->locale : "";
+ $form->dropdown("locale")
+ ->label(t("Language preference"))
+ ->options($locales)
+ ->selected($selected_locale);
+ }
+}
diff --git a/modules/user/css/user.css b/modules/user/css/user.css
new file mode 100644
index 0000000..93e3d02
--- /dev/null
+++ b/modules/user/css/user.css
@@ -0,0 +1,120 @@
+/* User- and group-related form width ~~~~ */
+
+#g-login-form,
+#g-add-user-form
+#g-edit-user-form,
+#g-delete-user-form,
+#g-user-admin {
+ width: 270px;
+}
+
+/* User/group admin ~~~~~~~~~~~~~~~~~~~~~~ */
+
+#g-user-admin {
+ width: auto;
+ margin-bottom: 4em;
+}
+
+#g-group-admin {
+}
+
+#g-user-admin-list .g-admin {
+ color: #55f;
+ font-weight: bold;
+}
+
+.g-group {
+ display: block;
+ border: 1px solid #999;
+ margin: 0 1em 1em 0;
+ padding: 0;
+ width: 200px;
+}
+
+.g-group h4 {
+ background-color: #eee;
+ border-bottom: 1px dashed #ccc;
+ padding: .5em 0 .5em .5em;
+}
+
+.g-group .g-button {
+ padding: 0;
+}
+
+.g-group .g-member-list,
+.g-group div {
+ height: 180px;
+ margin: 1px;
+ overflow: auto;
+}
+
+.g-group p {
+ margin-top: 1em;
+ padding: .5em;
+ text-align: center;
+}
+
+.g-group .g-user {
+ padding: .2em 0 0 .5em;
+}
+
+.g-group .g-user .g-button {
+ vertical-align: middle;
+}
+
+.g-default-group h4,
+.g-default-group .g-user {
+ color: #999;
+}
+
+.g-group.ui-droppable {
+ padding: 0 !important;
+}
+
+/* Password strength meter ~~~~~~~~~~~~~~~ */
+
+.g-password-strength0 {
+ background: url(../images/progressImg1.png) no-repeat 0 0;
+ width: 138px;
+ height: 7px;
+}
+
+.g-password-strength10 {
+ background-position:0 -7px;
+}
+
+.g-password-strength20 {
+ background-position:0 -14px;
+}
+
+.g-password-strength30 {
+ background-position:0 -21px;
+}
+
+.g-password-strength40 {
+ background-position:0 -28px;
+}
+
+.g-password-strength50 {
+ background-position:0 -35px;
+}
+
+.g-password-strength60 {
+ background-position:0 -42px;
+}
+
+.g-password-strength70 {
+ background-position:0 -49px;
+}
+
+.g-password-strength80 {
+ background-position:0 -56px;
+}
+
+.g-password-strength90 {
+ background-position:0 -63px;
+}
+
+.g-password-strength100 {
+ background-position:0 -70px;
+}
diff --git a/modules/user/helpers/group.php b/modules/user/helpers/group.php
new file mode 100644
index 0000000..59c9859
--- /dev/null
+++ b/modules/user/helpers/group.php
@@ -0,0 +1,82 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * This is the API for handling groups.
+ *
+ * Note: by design, this class does not do any permission checking.
+ */
+class group_Core {
+ /**
+ * The group of all possible visitors. This includes the guest user.
+ *
+ * @return Group_Definition the group object
+ */
+ static function everybody() {
+ return model_cache::get("group", 1);
+ }
+
+ /**
+ * The group of all logged-in visitors. This does not include guest users.
+ *
+ * @return Group_Definition the group object
+ */
+ static function registered_users() {
+ return model_cache::get("group", 2);
+ }
+
+ /**
+ * Look up a group by id.
+ * @param integer $id the user id
+ * @return Group_Definition the group object, or null if the id was invalid.
+ */
+ static function lookup($id) {
+ return self::_lookup_by_field("id", $id);
+ }
+
+ /**
+ * Look up a group by name.
+ * @param integer $id the group name
+ * @return Group_Definition the group object, or null if the name was invalid.
+ */
+ static function lookup_by_name($name) {
+ return self::_lookup_by_field("name", $name);
+ }
+
+ /**
+ * Search the groups by the field and value.
+ * @param string $field_name column to look up the user by
+ * @param string $value value to match
+ * @return Group_Definition the group object, or null if the name was invalid.
+ */
+ private static function _lookup_by_field($field_name, $value) {
+ try {
+ $group = model_cache::get("group", $value, $field_name);
+ if ($group->loaded()) {
+ return $group;
+ }
+ } catch (Exception $e) {
+ if (strpos($e->getMessage(), "MISSING_MODEL") === false) {
+ throw $e;
+ }
+ }
+ return null;
+ }
+}
diff --git a/modules/user/helpers/user.php b/modules/user/helpers/user.php
new file mode 100644
index 0000000..f59cf76
--- /dev/null
+++ b/modules/user/helpers/user.php
@@ -0,0 +1,176 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * This is the API for handling users.
+ *
+ * Note: by design, this class does not do any permission checking.
+ */
+class user_Core {
+ /**
+ * Return the guest user.
+ *
+ * @todo consider caching
+ *
+ * @return User_Model
+ */
+ static function guest() {
+ return model_cache::get("user", 1);
+ }
+
+ /**
+ * Return an admin user. Prefer the currently logged in user, if possible.
+ *
+ * @return User_Model
+ */
+ static function admin_user() {
+ $active = identity::active_user();
+ if ($active->admin) {
+ return $active;
+ }
+
+ return ORM::factory("user")->where("admin", "=", 1)->order_by("id", "ASC")->find();
+ }
+
+ /**
+ * Is the password provided correct?
+ *
+ * @param user User Model
+ * @param string $password a plaintext password
+ * @return boolean true if the password is correct
+ */
+ static function is_correct_password($user, $password) {
+ $valid = $user->password;
+
+ // Try phpass first, since that's what we generate.
+ if (strlen($valid) == 34) {
+ require_once(MODPATH . "user/lib/PasswordHash.php");
+ $hashGenerator = new PasswordHash(10, true);
+ return $hashGenerator->CheckPassword($password, $valid);
+ }
+
+ $salt = substr($valid, 0, 4);
+ // Support both old (G1 thru 1.4.0; G2 thru alpha-4) and new password schemes:
+ $guess = (strlen($valid) == 32) ? md5($password) : ($salt . md5($salt . $password));
+ if (!strcmp($guess, $valid)) {
+ return true;
+ }
+
+ // Passwords with <&"> created by G2 prior to 2.1 were hashed with entities
+ $sanitizedPassword = html::chars($password, false);
+ $guess = (strlen($valid) == 32) ? md5($sanitizedPassword)
+ : ($salt . md5($salt . $sanitizedPassword));
+ if (!strcmp($guess, $valid)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ static function valid_password($password_input) {
+ if (!user::is_correct_password(identity::active_user(), $password_input->value)) {
+ $password_input->add_error("invalid_password", 1);
+ }
+ }
+
+ static function valid_username($text_input) {
+ if (!self::lookup_by_name($text_input->value)) {
+ $text_input->add_error("invalid_username", 1);
+ }
+ }
+
+ /**
+ * Create the hashed passwords.
+ * @param string $password a plaintext password
+ * @return string hashed password
+ */
+ static function hash_password($password) {
+ require_once(MODPATH . "user/lib/PasswordHash.php");
+ $hashGenerator = new PasswordHash(10, true);
+ return $hashGenerator->HashPassword($password);
+ }
+
+ /**
+ * Look up a user by id.
+ * @param integer $id the user id
+ * @return User_Model the user object, or null if the id was invalid.
+ */
+ static function lookup($id) {
+ return self::_lookup_user_by_field("id", $id);
+ }
+
+ /**
+ * Look up a user by name.
+ * @param integer $name the user name
+ * @return User_Model the user object, or null if the name was invalid.
+ */
+ static function lookup_by_name($name) {
+ return self::_lookup_user_by_field("name", $name);
+ }
+
+ /**
+ * Look up a user by hash.
+ * @param integer $hash the user hash value
+ * @return User_Model the user object, or null if the name was invalid.
+ */
+ static function lookup_by_hash($hash) {
+ return self::_lookup_user_by_field("hash", $hash);
+ }
+
+ /**
+ * List the users
+ * @param mixed filters (@see Database.php
+ * @return array the user list.
+ */
+ static function get_user_list($filter=array()) {
+ $user = ORM::factory("user");
+
+ foreach($filter as $method => $args) {
+ switch ($method) {
+ case "in":
+ $user->in($args[0], $args[1]);
+ break;
+ default:
+ $user->$method($args);
+ }
+ }
+ return $user->find_all();
+ }
+
+ /**
+ * Look up a user by field value.
+ * @param string search field
+ * @param string search value
+ * @return User_Core the user object, or null if the name was invalid.
+ */
+ private static function _lookup_user_by_field($field_name, $value) {
+ try {
+ $user = model_cache::get("user", $value, $field_name);
+ if ($user->loaded()) {
+ return $user;
+ }
+ } catch (Exception $e) {
+ if (strpos($e->getMessage(), "MISSING_MODEL") === false) {
+ throw $e;
+ }
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/modules/user/helpers/user_event.php b/modules/user/helpers/user_event.php
new file mode 100644
index 0000000..40f6dde
--- /dev/null
+++ b/modules/user/helpers/user_event.php
@@ -0,0 +1,30 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+class user_event_Core {
+ static function admin_menu($menu, $theme) {
+ $menu->add_after("appearance_menu", Menu::factory("link")
+ ->id("users_groups")
+ ->label(t("Users/Groups"))
+ ->url(url::site("admin/users")));
+
+ return $menu;
+ }
+}
diff --git a/modules/user/helpers/user_installer.php b/modules/user/helpers/user_installer.php
new file mode 100644
index 0000000..67f6a3d
--- /dev/null
+++ b/modules/user/helpers/user_installer.php
@@ -0,0 +1,142 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class user_installer {
+ static function can_activate() {
+ return array("warn" => array(IdentityProvider::confirmation_message()));
+ }
+
+ static function activate() {
+ IdentityProvider::change_provider("user");
+ // Set the latest version in initialize() below
+ }
+
+ static function upgrade($version) {
+ if ($version == 1) {
+ module::set_var("user", "mininum_password_length", 5);
+ module::set_version("user", $version = 2);
+ }
+
+ if ($version == 2) {
+ db::build()
+ ->update("users")
+ ->set("email", "unknown@unknown.com")
+ ->where("guest", "=", 0)
+ ->and_open()
+ ->where("email", "IS", null)
+ ->or_where("email", "=", "")
+ ->close()
+ ->execute();
+ module::set_version("user", $version = 3);
+ }
+
+ if ($version == 3) {
+ $password_length = module::get_var("user", "mininum_password_length", 5);
+ module::set_var("user", "minimum_password_length", $password_length);
+ module::clear_var("user", "mininum_password_length");
+ module::set_version("user", $version = 4);
+ }
+ }
+
+ static function uninstall() {
+ // Delete all users and groups so that we give other modules an opportunity to clean up
+ foreach (ORM::factory("user")->find_all() as $user) {
+ $user->delete();
+ }
+
+ foreach (ORM::factory("group")->find_all() as $group) {
+ $group->delete();
+ }
+
+ $db = Database::instance();
+ $db->query("DROP TABLE IF EXISTS {users};");
+ $db->query("DROP TABLE IF EXISTS {groups};");
+ $db->query("DROP TABLE IF EXISTS {groups_users};");
+ }
+
+ static function initialize() {
+ $db = Database::instance();
+ $db->query("CREATE TABLE IF NOT EXISTS {users} (
+ `id` int(9) NOT NULL auto_increment,
+ `name` varchar(32) NOT NULL,
+ `full_name` varchar(255) NOT NULL,
+ `password` varchar(64) NOT NULL,
+ `login_count` int(10) unsigned NOT NULL DEFAULT 0,
+ `last_login` int(10) unsigned NOT NULL DEFAULT 0,
+ `email` varchar(64) default NULL,
+ `admin` BOOLEAN default 0,
+ `guest` BOOLEAN default 0,
+ `hash` char(32) default NULL,
+ `url` varchar(255) default NULL,
+ `locale` char(10) default NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY(`hash`),
+ UNIQUE KEY(`name`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE IF NOT EXISTS {groups} (
+ `id` int(9) NOT NULL auto_increment,
+ `name` char(64) default NULL,
+ `special` BOOLEAN default 0,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY(`name`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE IF NOT EXISTS {groups_users} (
+ `group_id` int(9) NOT NULL,
+ `user_id` int(9) NOT NULL,
+ PRIMARY KEY (`group_id`, `user_id`),
+ UNIQUE KEY(`user_id`, `group_id`))
+ DEFAULT CHARSET=utf8;");
+
+ $everybody = ORM::factory("group");
+ $everybody->name = "Everybody";
+ $everybody->special = true;
+ $everybody->save();
+
+ $registered = ORM::factory("group");
+ $registered->name = "Registered Users";
+ $registered->special = true;
+ $registered->save();
+
+ $guest = ORM::factory("user");
+ $guest->name = "guest";
+ $guest->full_name = "Guest User";
+ $guest->password = "";
+ $guest->guest = true;
+ $guest->save();
+
+ $admin = ORM::factory("user");
+ $admin->name = "admin";
+ $admin->full_name = "Gallery Administrator";
+ $admin->password = "admin";
+ $admin->email = "unknown@unknown.com";
+ $admin->admin = true;
+ $admin->save();
+
+ $root = ORM::factory("item", 1);
+ access::allow($everybody, "view", $root);
+ access::allow($everybody, "view_full", $root);
+
+ access::allow($registered, "view", $root);
+ access::allow($registered, "view_full", $root);
+
+ module::set_var("user", "minimum_password_length", 5);
+ }
+} \ No newline at end of file
diff --git a/modules/user/helpers/user_theme.php b/modules/user/helpers/user_theme.php
new file mode 100644
index 0000000..c608cdf
--- /dev/null
+++ b/modules/user/helpers/user_theme.php
@@ -0,0 +1,30 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class user_theme_Core {
+ static function head($theme) {
+ return $theme->css("user.css")
+ . $theme->script("password_strength.js");
+ }
+
+ static function admin_head($theme) {
+ return $theme->css("user.css")
+ . $theme->script("password_strength.js");
+ }
+} \ No newline at end of file
diff --git a/modules/user/images/progressImg1.png b/modules/user/images/progressImg1.png
new file mode 100644
index 0000000..a909364
--- /dev/null
+++ b/modules/user/images/progressImg1.png
Binary files differ
diff --git a/modules/user/js/password_strength.js b/modules/user/js/password_strength.js
new file mode 100644
index 0000000..2442b8d
--- /dev/null
+++ b/modules/user/js/password_strength.js
@@ -0,0 +1,39 @@
+(function($) {
+ // Based on the Password Strength Indictor By Benjamin Sterling
+ // http://benjaminsterling.com/password-strength-indicator-and-generator/
+ $.widget("ui.user_password_strength", {
+ _init: function() {
+ var self = this;
+ $(this.element).keyup(function() {
+ var strength = self.calculateStrength (this.value);
+ var index = Math.min(Math.floor( strength / 10 ), 10);
+ $("#g-password-gauge")
+ .removeAttr('class')
+ .addClass( "g-password-strength0" )
+ .addClass( self.options.classes[ index ] );
+ }).after("<div id='g-password-gauge' class='g-password-strength0'></div>");
+ },
+
+ calculateStrength: function(value) {
+ // Factor in the length of the password
+ var strength = Math.min(5, value.length) * 10 - 20;
+ // Factor in the number of numbers
+ strength += Math.min(3, value.length - value.replace(/[0-9]/g,"").length) * 10;
+ // Factor in the number of non word characters
+ strength += Math.min(3, value.length - value.replace(/\W/g,"").length) * 15;
+ // Factor in the number of Upper case letters
+ strength += Math.min(3, value.length - value.replace(/[A-Z]/g,"").length) * 10;
+
+ // Normalizxe between 0 and 100
+ return Math.max(0, Math.min(100, strength));
+ }
+ });
+ $.extend($.ui.user_password_strength, {
+ defaults: {
+ classes : ['g-password-strength10', 'g-password-strength20', 'g-password-strength30',
+ 'g-password-strength40', 'g-password-strength50', 'g-password-strength60',
+ 'g-password-strength70',' g-password-strength80',' g-password-strength90',
+ 'g-password-strength100']
+ }
+ });
+ })(jQuery);
diff --git a/modules/user/lib/PasswordHash.php b/modules/user/lib/PasswordHash.php
new file mode 100644
index 0000000..d6783c0
--- /dev/null
+++ b/modules/user/lib/PasswordHash.php
@@ -0,0 +1,248 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+#
+# Portable PHP password hashing framework.
+#
+# Version 0.1 / genuine.
+#
+# Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in
+# the public domain.
+#
+# There's absolutely no warranty.
+#
+# The homepage URL for this framework is:
+#
+# http://www.openwall.com/phpass/
+#
+# Please be sure to update the Version line if you edit this file in any way.
+# It is suggested that you leave the main version number intact, but indicate
+# your project name (after the slash) and add your own revision information.
+#
+# Please do not change the "private" password hashing method implemented in
+# here, thereby making your hashes incompatible. However, if you must, please
+# change the hash type identifier (the "$P$") to something different.
+#
+# Obviously, since this code is in the public domain, the above are not
+# requirements (there can be none), but merely suggestions.
+#
+class PasswordHash {
+ var $itoa64;
+ var $iteration_count_log2;
+ var $portable_hashes;
+ var $random_state;
+
+ function PasswordHash($iteration_count_log2, $portable_hashes)
+ {
+ $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+
+ if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31)
+ $iteration_count_log2 = 8;
+ $this->iteration_count_log2 = $iteration_count_log2;
+
+ $this->portable_hashes = $portable_hashes;
+
+ $this->random_state = microtime() . getmypid();
+ }
+
+ function get_random_bytes($count)
+ {
+ $output = '';
+ if (($fh = @fopen('/dev/urandom', 'rb'))) {
+ $output = fread($fh, $count);
+ fclose($fh);
+ }
+
+ if (strlen($output) < $count) {
+ $output = '';
+ for ($i = 0; $i < $count; $i += 16) {
+ $this->random_state =
+ md5(microtime() . $this->random_state);
+ $output .=
+ pack('H*', md5($this->random_state));
+ }
+ $output = substr($output, 0, $count);
+ }
+
+ return $output;
+ }
+
+ function encode64($input, $count)
+ {
+ $output = '';
+ $i = 0;
+ do {
+ $value = ord($input[$i++]);
+ $output .= $this->itoa64[$value & 0x3f];
+ if ($i < $count)
+ $value |= ord($input[$i]) << 8;
+ $output .= $this->itoa64[($value >> 6) & 0x3f];
+ if ($i++ >= $count)
+ break;
+ if ($i < $count)
+ $value |= ord($input[$i]) << 16;
+ $output .= $this->itoa64[($value >> 12) & 0x3f];
+ if ($i++ >= $count)
+ break;
+ $output .= $this->itoa64[($value >> 18) & 0x3f];
+ } while ($i < $count);
+
+ return $output;
+ }
+
+ function gensalt_private($input)
+ {
+ $output = '$P$';
+ $output .= $this->itoa64[min($this->iteration_count_log2 +
+ ((PHP_VERSION >= '5') ? 5 : 3), 30)];
+ $output .= $this->encode64($input, 6);
+
+ return $output;
+ }
+
+ function crypt_private($password, $setting)
+ {
+ $output = '*0';
+ if (substr($setting, 0, 2) == $output)
+ $output = '*1';
+
+ if (substr($setting, 0, 3) != '$P$')
+ return $output;
+
+ $count_log2 = strpos($this->itoa64, $setting[3]);
+ if ($count_log2 < 7 || $count_log2 > 30)
+ return $output;
+
+ $count = 1 << $count_log2;
+
+ $salt = substr($setting, 4, 8);
+ if (strlen($salt) != 8)
+ return $output;
+
+ # We're kind of forced to use MD5 here since it's the only
+ # cryptographic primitive available in all versions of PHP
+ # currently in use. To implement our own low-level crypto
+ # in PHP would result in much worse performance and
+ # consequently in lower iteration counts and hashes that are
+ # quicker to crack (by non-PHP code).
+ if (PHP_VERSION >= '5') {
+ $hash = md5($salt . $password, TRUE);
+ do {
+ $hash = md5($hash . $password, TRUE);
+ } while (--$count);
+ } else {
+ $hash = pack('H*', md5($salt . $password));
+ do {
+ $hash = pack('H*', md5($hash . $password));
+ } while (--$count);
+ }
+
+ $output = substr($setting, 0, 12);
+ $output .= $this->encode64($hash, 16);
+
+ return $output;
+ }
+
+ function gensalt_extended($input)
+ {
+ $count_log2 = min($this->iteration_count_log2 + 8, 24);
+ # This should be odd to not reveal weak DES keys, and the
+ # maximum valid value is (2**24 - 1) which is odd anyway.
+ $count = (1 << $count_log2) - 1;
+
+ $output = '_';
+ $output .= $this->itoa64[$count & 0x3f];
+ $output .= $this->itoa64[($count >> 6) & 0x3f];
+ $output .= $this->itoa64[($count >> 12) & 0x3f];
+ $output .= $this->itoa64[($count >> 18) & 0x3f];
+
+ $output .= $this->encode64($input, 3);
+
+ return $output;
+ }
+
+ function gensalt_blowfish($input)
+ {
+ # This one needs to use a different order of characters and a
+ # different encoding scheme from the one in encode64() above.
+ # We care because the last character in our encoded string will
+ # only represent 2 bits. While two known implementations of
+ # bcrypt will happily accept and correct a salt string which
+ # has the 4 unused bits set to non-zero, we do not want to take
+ # chances and we also do not want to waste an additional byte
+ # of entropy.
+ $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+ $output = '$2a$';
+ $output .= chr(ord('0') + $this->iteration_count_log2 / 10);
+ $output .= chr(ord('0') + $this->iteration_count_log2 % 10);
+ $output .= '$';
+
+ $i = 0;
+ do {
+ $c1 = ord($input[$i++]);
+ $output .= $itoa64[$c1 >> 2];
+ $c1 = ($c1 & 0x03) << 4;
+ if ($i >= 16) {
+ $output .= $itoa64[$c1];
+ break;
+ }
+
+ $c2 = ord($input[$i++]);
+ $c1 |= $c2 >> 4;
+ $output .= $itoa64[$c1];
+ $c1 = ($c2 & 0x0f) << 2;
+
+ $c2 = ord($input[$i++]);
+ $c1 |= $c2 >> 6;
+ $output .= $itoa64[$c1];
+ $output .= $itoa64[$c2 & 0x3f];
+ } while (1);
+
+ return $output;
+ }
+
+ function HashPassword($password)
+ {
+ $random = '';
+
+ if (CRYPT_BLOWFISH == 1 && !$this->portable_hashes) {
+ $random = $this->get_random_bytes(16);
+ $hash =
+ crypt($password, $this->gensalt_blowfish($random));
+ if (strlen($hash) == 60)
+ return $hash;
+ }
+
+ if (CRYPT_EXT_DES == 1 && !$this->portable_hashes) {
+ if (strlen($random) < 3)
+ $random = $this->get_random_bytes(3);
+ $hash =
+ crypt($password, $this->gensalt_extended($random));
+ if (strlen($hash) == 20)
+ return $hash;
+ }
+
+ if (strlen($random) < 6)
+ $random = $this->get_random_bytes(6);
+ $hash =
+ $this->crypt_private($password,
+ $this->gensalt_private($random));
+ if (strlen($hash) == 34)
+ return $hash;
+
+ # Returning '*' on error is safe here, but would _not_ be safe
+ # in a crypt(3)-like function used _both_ for generating new
+ # hashes and for validating passwords against existing hashes.
+ return '*';
+ }
+
+ function CheckPassword($password, $stored_hash)
+ {
+ $hash = $this->crypt_private($password, $stored_hash);
+ if ($hash[0] == '*')
+ $hash = crypt($password, $stored_hash);
+
+ return $hash == $stored_hash;
+ }
+}
+
+?>
diff --git a/modules/user/libraries/drivers/IdentityProvider/Gallery.php b/modules/user/libraries/drivers/IdentityProvider/Gallery.php
new file mode 100644
index 0000000..67da33d
--- /dev/null
+++ b/modules/user/libraries/drivers/IdentityProvider/Gallery.php
@@ -0,0 +1,164 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+/*
+ * Based on the Cache_Sqlite_Driver developed by the Kohana Team
+ */
+class IdentityProvider_Gallery_Driver implements IdentityProvider_Driver {
+ /**
+ * @see IdentityProvider_Driver::guest.
+ */
+ public function guest() {
+ return user::guest();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::guest.
+ */
+ public function admin_user() {
+ return user::admin_user();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::create_user.
+ */
+ public function create_user($name, $full_name, $password, $email) {
+ $user = ORM::factory("user");
+ $user->name = $name;
+ $user->full_name = $full_name;
+ $user->password = $password;
+ $user->email = $email;
+ return $user->save();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::is_correct_password.
+ */
+ public function is_correct_password($user, $password) {
+ $valid = $user->password;
+
+ // Try phpass first, since that's what we generate.
+ if (strlen($valid) == 34) {
+ require_once(MODPATH . "user/lib/PasswordHash.php");
+ $hashGenerator = new PasswordHash(10, true);
+ return $hashGenerator->CheckPassword($password, $valid);
+ }
+
+ $salt = substr($valid, 0, 4);
+ // Support both old (G1 thru 1.4.0; G2 thru alpha-4) and new password schemes:
+ $guess = (strlen($valid) == 32) ? md5($password) : ($salt . md5($salt . $password));
+ if (!strcmp($guess, $valid)) {
+ return true;
+ }
+
+ // Passwords with <&"> created by G2 prior to 2.1 were hashed with entities
+ $sanitizedPassword = html::chars($password, false);
+ $guess = (strlen($valid) == 32) ? md5($sanitizedPassword)
+ : ($salt . md5($salt . $sanitizedPassword));
+ if (!strcmp($guess, $valid)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @see IdentityProvider_Driver::lookup_user.
+ */
+ public function lookup_user($id) {
+ return user::lookup($id);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::lookup_user_by_name.
+ */
+ public function lookup_user_by_name($name) {
+ return user::lookup_by_name($name);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::create_group.
+ */
+ public function create_group($name) {
+ $group = ORM::factory("group");
+ $group->name = $name;
+ return $group->save();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::everybody.
+ */
+ public function everybody() {
+ return group::everybody();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::registered_users.
+ */
+ public function registered_users() {
+ return group::registered_users();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::lookup_group.
+ */
+ public function lookup_group($id) {
+ return group::lookup($id);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::lookup_group_by_name.
+ */
+ public function lookup_group_by_name($name) {
+ return group::lookup_by_name($name);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::get_user_list.
+ */
+ public function get_user_list($ids) {
+ return ORM::factory("user")
+ ->where("id", "IN", $ids)
+ ->find_all();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::groups.
+ */
+ public function groups() {
+ return ORM::factory("group")->find_all();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::add_user_to_group.
+ */
+ public function add_user_to_group($user, $group) {
+ $group->add($user);
+ $group->save();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::remove_user_to_group.
+ */
+ public function remove_user_from_group($user, $group) {
+ $group->remove($user);
+ $group->save();
+ }
+} // End Identity Gallery Driver
+
diff --git a/modules/user/models/group.php b/modules/user/models/group.php
new file mode 100644
index 0000000..a5d7c5b
--- /dev/null
+++ b/modules/user/models/group.php
@@ -0,0 +1,89 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class Group_Model_Core extends ORM implements Group_Definition {
+ protected $has_and_belongs_to_many = array("users");
+ protected $users_cache = null;
+
+ /**
+ * @see ORM::delete()
+ */
+ public function delete($id=null) {
+ $old = clone $this;
+ module::event("group_before_delete", $this);
+ parent::delete($id);
+
+ db::build()
+ ->delete("groups_users")
+ ->where("group_id", "=", empty($id) ? $old->id : $id)
+ ->execute();
+
+ module::event("group_deleted", $old);
+ $this->users_cache = null;
+ }
+
+ public function users() {
+ if (!$this->users_cache) {
+ $this->users_cache = $this->users->find_all()->as_array();
+ }
+ return $this->users_cache;
+ }
+
+ /**
+ * Specify our rules here so that we have access to the instance of this model.
+ */
+ public function validate(Validation $array=null) {
+ // validate() is recursive, only modify the rules on the outermost call.
+ if (!$array) {
+ $this->rules = array(
+ "name" => array("rules" => array("required", "length[1,255]"),
+ "callbacks" => array(array($this, "valid_name"))));
+ }
+
+ parent::validate($array);
+ }
+
+ public function save() {
+ if (!$this->loaded()) {
+ // New group
+ parent::save();
+ module::event("group_created", $this);
+ } else {
+ // Updated group
+ $original = ORM::factory("group", $this->id);
+ parent::save();
+ module::event("group_updated", $original, $this);
+ }
+
+ $this->users_cache = null;
+ return $this;
+ }
+
+ /**
+ * Validate the user name. Make sure there are no conflicts.
+ */
+ public function valid_name(Validation $v, $field) {
+ if (db::build()->from("groups")
+ ->where("name", "=", $this->name)
+ ->where("id", "<>", $this->id)
+ ->count_records() == 1) {
+ $v->add_error("name", "conflict");
+ }
+ }
+} \ No newline at end of file
diff --git a/modules/user/models/user.php b/modules/user/models/user.php
new file mode 100644
index 0000000..af05c0c
--- /dev/null
+++ b/modules/user/models/user.php
@@ -0,0 +1,185 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class User_Model_Core extends ORM implements User_Definition {
+ protected $has_and_belongs_to_many = array("groups");
+ protected $password_length = null;
+ protected $groups_cache = null;
+
+ public function __set($column, $value) {
+ switch ($column) {
+ case "hashed_password":
+ $column = "password";
+ break;
+
+ case "password":
+ $this->password_length = strlen($value);
+ $value = user::hash_password($value);
+ break;
+ }
+ parent::__set($column, $value);
+ }
+
+ /**
+ * @see ORM::delete()
+ */
+ public function delete($id=null) {
+ $old = clone $this;
+ module::event("user_before_delete", $this);
+ parent::delete($id);
+
+ db::build()
+ ->delete("groups_users")
+ ->where("user_id", "=", empty($id) ? $old->id : $id)
+ ->execute();
+
+ module::event("user_deleted", $old);
+ $this->groups_cache = null;
+ }
+
+ /**
+ * Return a url to the user's avatar image.
+ * @param integer $size the target size of the image (default 80px)
+ * @return string a url
+ */
+ public function avatar_url($size=80, $default=null) {
+ return sprintf("http://www.gravatar.com/avatar/%s.jpg?s=%d&r=pg%s",
+ md5($this->email), $size, $default ? "&d=" . urlencode($default) : "");
+ }
+
+ public function groups() {
+ if (!$this->groups_cache) {
+ $this->groups_cache = $this->groups->find_all()->as_array();
+ }
+ return $this->groups_cache;
+ }
+
+ /**
+ * Specify our rules here so that we have access to the instance of this model.
+ */
+ public function validate(Validation $array=null) {
+ // validate() is recursive, only modify the rules on the outermost call.
+ if (!$array) {
+ $this->rules = array(
+ "admin" => array("callbacks" => array(array($this, "valid_admin"))),
+ "email" => array("rules" => array("length[1,255]", "valid::email"),
+ "callbacks" => array(array($this, "valid_email"))),
+ "full_name" => array("rules" => array("length[0,255]")),
+ "locale" => array("rules" => array("length[2,10]")),
+ "name" => array("rules" => array("length[1,32]", "required"),
+ "callbacks" => array(array($this, "valid_name"))),
+ "password" => array("callbacks" => array(array($this, "valid_password"))),
+ "url" => array("rules" => array("valid::url")),
+ );
+ }
+
+ parent::validate($array);
+ }
+
+ /**
+ * Handle any business logic necessary to create or update a user.
+ * @see ORM::save()
+ *
+ * @return ORM User_Model
+ */
+ public function save() {
+ if ($this->full_name === null) {
+ $this->full_name = "";
+ }
+
+ if (!$this->loaded()) {
+ // New user
+ $this->add(group::everybody());
+ if (!$this->guest) {
+ $this->add(group::registered_users());
+ }
+
+ parent::save();
+ module::event("user_created", $this);
+ } else {
+ // Updated user
+ $original = ORM::factory("user", $this->id);
+ parent::save();
+ module::event("user_updated", $original, $this);
+ }
+
+ $this->groups_cache = null;
+ return $this;
+ }
+
+ /**
+ * Return the best version of the user's name. Either their specified full name, or fall back
+ * to the user name.
+ * @return string
+ */
+ public function display_name() {
+ return empty($this->full_name) ? $this->name : $this->full_name;
+ }
+
+ /**
+ * Validate the user name. Make sure there are no conflicts.
+ */
+ public function valid_name(Validation $v, $field) {
+ if (db::build()->from("users")
+ ->where("name", "=", $this->name)
+ ->merge_where($this->id ? array(array("id", "<>", $this->id)) : null)
+ ->count_records() == 1) {
+ $v->add_error("name", "conflict");
+ }
+ }
+
+ /**
+ * Validate the password.
+ */
+ public function valid_password(Validation $v, $field) {
+ if ($this->guest) {
+ return;
+ }
+
+ if (!$this->loaded() || isset($this->password_length)) {
+ $minimum_length = module::get_var("user", "minimum_password_length", 5);
+ if ($this->password_length < $minimum_length) {
+ $v->add_error("password", "min_length");
+ }
+ }
+ }
+
+ /**
+ * Validate the admin bit.
+ */
+ public function valid_admin(Validation $v, $field) {
+ $active = identity::active_user();
+ if ($this->id == $active->id && $active->admin && !$this->admin) {
+ $v->add_error("admin", "locked");
+ }
+ }
+
+ /**
+ * Validate the email field.
+ */
+ public function valid_email(Validation $v, $field) {
+ if ($this->guest) { // guests don't require an email address
+ return;
+ }
+
+ if (empty($this->email)) {
+ $v->add_error("email", "required");
+ }
+ }
+}
diff --git a/modules/user/module.info b/modules/user/module.info
new file mode 100644
index 0000000..d5128db
--- /dev/null
+++ b/modules/user/module.info
@@ -0,0 +1,8 @@
+name = "Users and Groups"
+description = "Gallery 3 user and group management"
+version = 4
+
+author_name = "Gallery Team"
+author_url = "http://codex.galleryproject.org/Gallery:Team"
+info_url = "http://codex.galleryproject.org/Gallery3:Modules:user"
+discuss_url = "http://galleryproject.org/forum_module_user"
diff --git a/modules/user/views/admin_users.html.php b/modules/user/views/admin_users.html.php
new file mode 100644
index 0000000..033c9da
--- /dev/null
+++ b/modules/user/views/admin_users.html.php
@@ -0,0 +1,142 @@
+<?php defined("SYSPATH") or die("No direct script access.") ?>
+<script type="text/javascript">
+ var add_user_to_group_url = "<?= url::site("admin/users/add_user_to_group/__USERID__/__GROUPID__?csrf=$csrf") ?>";
+ $(document).ready(function(){
+ $("#g-user-admin-list .g-core-info").draggable({
+ helper: "clone"
+ });
+ $("#g-group-admin .g-group").droppable({
+ accept: ".g-core-info",
+ hoverClass: "g-selected",
+ drop: function(ev, ui) {
+ var user_id = $(ui.draggable).attr("id").replace("g-user-", "");
+ var group_id = $(this).attr("id").replace("g-group-", "");
+ $.get(add_user_to_group_url.replace("__USERID__", user_id).replace("__GROUPID__", group_id),
+ {},
+ function() {
+ reload_group(group_id);
+ });
+ }
+ });
+ $("#g-group-1").droppable("destroy");
+ $("#g-group-2").droppable("destroy");
+ });
+
+ var reload_group = function(group_id) {
+ var reload_group_url = "<?= url::site("admin/users/group/__GROUPID__") ?>";
+ $.get(reload_group_url.replace("__GROUPID__", group_id),
+ {},
+ function(data) {
+ $("#g-group-" + group_id).html(data);
+ $("#g-group-" + group_id + " .g-dialog-link").gallery_dialog();
+ });
+ }
+
+ var remove_user = function(user_id, group_id) {
+ var remove_user_url = "<?= url::site("admin/users/remove_user_from_group/__USERID__/__GROUPID__?csrf=$csrf") ?>";
+ $.get(remove_user_url.replace("__USERID__", user_id).replace("__GROUPID__", group_id),
+ {},
+ function() {
+ reload_group(group_id);
+ });
+ }
+</script>
+
+<div class="g-block">
+ <h1> <?= t("Users and groups") ?> </h1>
+
+ <div class="g-block-content">
+
+ <div id="g-user-admin" class="g-block">
+ <a href="<?= url::site("admin/users/add_user_form") ?>"
+ class="g-dialog-link g-button g-right ui-icon-left ui-state-default ui-corner-all"
+ title="<?= t("Create a new user")->for_html_attr() ?>">
+ <span class="ui-icon ui-icon-circle-plus"></span>
+ <?= t("Add a new user") ?>
+ </a>
+
+ <h2> <?= t("Users") ?> </h2>
+
+ <div class="g-block-content">
+ <table id="g-user-admin-list">
+ <tr>
+ <th><?= t("Username") ?></th>
+ <th><?= t("Full name") ?></th>
+ <th><?= t("Email") ?></th>
+ <th><?= t("Last login") ?></th>
+ <th><?= t("Albums/Photos") ?></th>
+ <th><?= t("Actions") ?></th>
+ </tr>
+
+ <? foreach ($users as $i => $user): ?>
+ <tr id="g-user-<?= $user->id ?>" class="<?= text::alternate("g-odd", "g-even") ?> g-user <?= $user->admin ? "g-admin" : "" ?>">
+ <td id="g-user-<?= $user->id ?>" class="g-core-info g-draggable">
+ <img src="<?= $user->avatar_url(20, $theme->url("images/avatar.jpg", true)) ?>"
+ title="<?= t("Drag user onto a group to add as a new member")->for_html_attr() ?>"
+ alt="<?= html::clean_attribute($user->name) ?>"
+ width="20"
+ height="20" />
+ <?= html::clean($user->name) ?>
+ </td>
+ <td>
+ <?= html::clean($user->full_name) ?>
+ </td>
+ <td>
+ <?= html::clean($user->email) ?>
+ </td>
+ <td>
+ <?= ($user->last_login == 0) ? "" : gallery::date($user->last_login) ?>
+ </td>
+ <td>
+ <?= db::build()->from("items")->where("owner_id", "=", $user->id)->count_records() ?>
+ </td>
+ <td>
+ <a href="<?= url::site("admin/users/edit_user_form/$user->id") ?>"
+ open_text="<?= t("Close") ?>"
+ class="g-panel-link g-button ui-state-default ui-corner-all ui-icon-left">
+ <span class="ui-icon ui-icon-pencil"></span><span class="g-button-text"><?= t("Edit") ?></span></a>
+ <? if (identity::active_user()->id != $user->id && !$user->guest): ?>
+ <a href="<?= url::site("admin/users/delete_user_form/$user->id") ?>"
+ class="g-dialog-link g-button ui-state-default ui-corner-all ui-icon-left">
+ <span class="ui-icon ui-icon-trash"></span><?= t("Delete") ?></a>
+ <? else: ?>
+ <span title="<?= t("This user cannot be deleted")->for_html_attr() ?>"
+ class="g-button ui-state-disabled ui-corner-all ui-icon-left">
+ <span class="ui-icon ui-icon-trash"></span><?= t("Delete") ?></span>
+ <? endif ?>
+ </td>
+ </tr>
+ <? endforeach ?>
+ </table>
+
+ <div class="g-paginator">
+ <?= $theme->paginator() ?>
+ </div>
+
+ </div>
+ </div>
+
+ <div id="g-group-admin" class="g-block ui-helper-clearfix">
+ <a href="<?= url::site("admin/users/add_group_form") ?>"
+ class="g-dialog-link g-button g-right ui-icon-left ui-state-default ui-corner-all"
+ title="<?= t("Create a new group")->for_html_attr() ?>">
+ <span class="ui-icon ui-icon-circle-plus"></span>
+ <?= t("Add a new group") ?>
+ </a>
+
+ <h2> <?= t("Groups") ?> </h2>
+
+ <div class="g-block-content">
+ <ul>
+ <? foreach ($groups as $i => $group): ?>
+ <li id="g-group-<?= $group->id ?>" class="g-group g-left <?= ($group->special ? "g-default-group" : "") ?>">
+ <? $v = new View("admin_users_group.html"); $v->group = $group; ?>
+ <?= $v ?>
+ </li>
+ <? endforeach ?>
+ </ul>
+ </div>
+ </div>
+
+ </div>
+</div>
diff --git a/modules/user/views/admin_users_delete_user.html.php b/modules/user/views/admin_users_delete_user.html.php
new file mode 100644
index 0000000..44777ae
--- /dev/null
+++ b/modules/user/views/admin_users_delete_user.html.php
@@ -0,0 +1,7 @@
+<?php defined("SYSPATH") or die("No direct script access.") ?>
+<div id="g-admin-users-delete-user">
+ <p>
+ <?= t("Really delete <b>%name</b>? Any photos, movies or albums owned by this user will transfer ownership to <b>%new_owner</b>.", array("name" => $user->display_name(), "new_owner" => identity::active_user()->display_name())) ?>
+ </p>
+ <?= $form ?>
+</div>
diff --git a/modules/user/views/admin_users_group.html.php b/modules/user/views/admin_users_group.html.php
new file mode 100644
index 0000000..31b9135
--- /dev/null
+++ b/modules/user/views/admin_users_group.html.php
@@ -0,0 +1,40 @@
+<?php defined("SYSPATH") or die("No direct script access.") ?>
+<h4>
+ <a href="<?= url::site("admin/users/edit_group_form/$group->id") ?>"
+ title="<?= t("Edit the %name group's name", array("name" => $group->name))->for_html_attr() ?>"
+ class="g-dialog-link"><?= html::clean($group->name) ?></a>
+ <? if (!$group->special): ?>
+ <a href="<?= url::site("admin/users/delete_group_form/$group->id") ?>"
+ title="<?= t("Delete the %name group", array("name" => $group->name))->for_html_attr() ?>"
+ class="g-dialog-link g-button g-right">
+ <span class="ui-icon ui-icon-trash"><?= t("Delete") ?></span></a>
+ <? else: ?>
+ <a title="<?= t("This default group cannot be deleted")->for_html_attr() ?>"
+ class="g-button g-right ui-state-disabled ui-icon-left">
+ <span class="ui-icon ui-icon-trash"><?= t("Delete") ?></span></a>
+ <? endif ?>
+</h4>
+
+<? if ($group->users->count_all() > 0): ?>
+<ul class="g-member-list">
+ <? foreach ($group->users->order_by("name", "ASC")->find_all() as $i => $user): ?>
+ <li class="g-user">
+ <?= html::clean($user->name) ?>
+ <? if (!$group->special): ?>
+ <a href="javascript:remove_user(<?= $user->id ?>, <?= $group->id ?>)"
+ class="g-button g-right ui-state-default ui-corner-all ui-icon-left"
+ title="<?= t("Remove %user from %group group",
+ array("user" => $user->name, "group" => $group->name))->for_html_attr() ?>">
+ <span class="ui-icon ui-icon-closethick"><?= t("Remove") ?></span>
+ </a>
+ <? endif ?>
+ </li>
+ <? endforeach ?>
+</ul>
+<? else: ?>
+<div>
+ <p class="ui-state-disabled">
+ <?= t("Drag &amp; drop users from the \"Users\" list onto this group to add group members.") ?>
+ </p>
+</div>
+<? endif ?>
diff --git a/modules/user/views/reset_password.html.php b/modules/user/views/reset_password.html.php
new file mode 100644
index 0000000..d939ad4
--- /dev/null
+++ b/modules/user/views/reset_password.html.php
@@ -0,0 +1,18 @@
+<?php defined("SYSPATH") or die("No direct script access.") ?>
+<html>
+ <head>
+ <title><?= t("Password reset request") ?> </title>
+ </head>
+ <body>
+ <h2><?= t("Password reset request") ?> </h2>
+ <p>
+ <?= t("Hello, %name,", array("name" => $user->full_name ? $user->full_name : $user->name)) ?>
+ </p>
+ <p>
+ <?= t("We received a request to reset your password for <a href=\"%site_url\">%base_url</a>. If you made this request, you can confirm it by <a href=\"%confirm_url\">clicking this link</a>. If you didn't request this password reset, it's ok to ignore this mail.",
+ array("site_url" => html::mark_clean(url::abs_site("/")),
+ "base_url" => html::mark_clean(url::base(false)),
+ "confirm_url" => $confirm_url)) ?>
+ </p>
+ </body>
+</html>