diff options
Diffstat (limited to 'modules/user')
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 Binary files differnew file mode 100644 index 0000000..a909364 --- /dev/null +++ b/modules/user/images/progressImg1.png 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 & 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> |
