#author("2022-03-21T18:24:35+09:00;2022-03-21T17:16:42+09:00","default:umorigu","umorigu")
#author("2022-05-10T22:05:09+09:00","","")
* 誰でもユーザーを登録できるようにする [#u9b84c47]

- ページ: [[BugTrack]]
- 投稿者: [[零鐵]]
- 優先順位: 低
- 状態: 提案
- カテゴリー: 本体新機能
- 投稿日: 2022-01-07 (金) 14:51:15
- バージョン: 1.5.3

** メッセージ [#zeeb233c]
誰でもユーザーを登録できるようにし、 %%Wikipediaにあるような利用者ページと下書きのような機能が欲しいです。 ([[BugTrack/2554]]に分離)%% 
登録ユーザーへ個別に権限の設定ができるといいと思ってます XD

** ユーザー登録システムの実装案 ([[はいふん]]) [#e7153aa7]

:lib/auth.php|
ユーザー登録に関する関数をつくってみました。~
私はform_auth関数の次に埋め込んでます。
 
 /**
  * Check if username can be used
  *
  * @param String username
  * @return true is usable username
  */
 function is_usable_username($username)
 {
 	global $auth_users;
 	if (in_array($username, array_keys($auth_users))) {
 		return false;
 	}
 	return true;
 }
 
 function exist_user($username) {
 	return !is_usable_username($username);
 }
 
 /**
  * User registration
  */
 function user_registration()
 {
 	global $user_registration;
 	global $auth_users, $created_users;
 	if ($user_registration) {
 		if (file_exists(PKWK_USERLIST_CACHE)) {
 			$lines = file(PKWK_USERLIST_CACHE);
 			foreach ($lines as $line) {
 				list($user, $pass) = explode("\t", rtrim($line));
 				$created_users[$user] = array(
 					'edit_auth_pages' => $pass,
 					'password' => $pass,
 				);
 				$created_auth_users[$user] = $pass;
 			}
 			if ($created_auth_users) {
 				$auth_users += $created_auth_users;
 				unset($created_auth_users);
 			}
 		}
 	}
 }
 
 /**
  * Create user
  *
  * @param String username
  * @param String password
  * @param boolean check
  * @return true is created user
  */
 function create_user($username, $password, $check)
 {
 	global $user_registration, $created_users;
 	if (!$user_registration)
 		die_message('User registration is disabled.');
 	if ($username == '' || $password == '') return false;
 	if ($check) {
 		if (exist_user($username)) {
 			return false;
 		}
 	}
 	$created_users[$username] = array(
 		'password' => $password,
 	); 
 	$file = PKWK_USERLIST_CACHE;
 	pkwk_touch_file($file);
 	$fp = fopen($file, 'r+') or
 		die_message('Cannot open ' . $file);
 	set_file_buffer($fp, 0);
 	flock($fp, LOCK_EX);
 	ftruncate($fp, 0);
 	rewind($fp);
 	foreach ($created_users as $user=>$data)
 		fputs($fp, $user . "\t" . $data['password'] . "\n");
 	flock($fp, LOCK_UN);
 	fclose($fp);
 	return true;
 }
form_auth関数の変更箇所
 function form_auth($username, $password)
 {
 	global $ldap_user_account, $auth_users;
 	$user = $username;
 	if ($ldap_user_account) {
 		// LDAP account
 		return ldap_auth($username, $password);
 	} else {
 		// Defined users in pukiwiki.ini.php
 -		if (in_array($user, array_keys($auth_users))) {
 +		if (exist_user($user)) {
 			if (pkwk_hash_compute(
 				$password,
 				$auth_users[$user]) === $auth_users[$user]) {
 				session_start();
 				session_regenerate_id(true); // require: PHP5.1+
 				$_SESSION['authenticated_user'] = $user;
 				$_SESSION['authenticated_user_fullname'] = $user;
 				return true;
 			}
 		}
 	}
 	return false;
 }
定数の定義
 /////////////////////////////////////////////////
 // User registration
 define('PKWK_USERLIST_CACHE', DATA_HOME . 'userlist.dat');

:lib/pukiwiki.php|
 /////////////////////////////////////////////////
 // Main
 if ($vars['page'] === FALSE) {
 	die_invalid_pagename();
 	exit;
 }
 if (manage_page_redirect()) {
 	exit;
 }
 $retvars = array();
 $is_cmd = FALSE;
 if (isset($vars['cmd'])) {
 	$is_cmd  = TRUE;
 	$plugin = & $vars['cmd'];
 } else if (isset($vars['plugin'])) {
 	$plugin = & $vars['plugin'];
 } else {
 	$plugin = '';
 }
 if ($plugin != '') {
 +	user_registration();
 	ensure_valid_auth_user();

:plugin/loginform.inc.php|
 function plugin_loginform_action()
 {
 	global $auth_user, $auth_type, $_loginform_messages;
 	$page = isset($_GET['page']) ? $_GET['page'] : '';
 	$pcmd = isset($_GET['pcmd']) ? $_GET['pcmd'] : '';
 	$url_after_login = isset($_GET['url_after_login']) ? $_GET['url_after_login'] : '';
 	$page_after_login = $page;
 	if (!$url_after_login) {
 		$page_after_login = $page;
 	}
 -	$action_url = get_base_uri() . '?plugin=loginform'
 -		. '&page=' . rawurlencode($page)
 -		. ($url_after_login ? '&url_after_login=' . rawurlencode($url_after_login) : '')
 -		. ($page_after_login ? '&page_after_login=' . rawurlencode($page_after_login) : '');
 	$username = isset($_POST['username']) ? $_POST['username'] : '';
 	$password = isset($_POST['password']) ? $_POST['password'] : '';
 	$isset_user_credential = $username || $password ;
 	if ($username && $password && form_auth($username, $password)) {
 		// Sign in successfully completed
 		form_auth_redirect($url_after_login, $page_after_login);
 		exit; // or 'return FALSE;' - Don't double check for FORM_AUTH
 	}
 	if ($pcmd === 'logout') {
 		// logout
 		switch ($auth_type) {
 			case AUTH_TYPE_BASIC:
 				header('WWW-Authenticate: Basic realm="Please cancel to log out"');
 				header('HTTP/1.0 401 Unauthorized');
 				break;
 			case AUTH_TYPE_FORM:
 			case AUTH_TYPE_EXTERNAL:
 			case AUTH_TYPE_SAML:
 			default:
 				$_SESSION = array();
 				session_regenerate_id(true); // require: PHP5.1+
 				session_destroy();
 				break;
 		}
 		$auth_user = '';
 		return array(
 			'msg' => 'Log out',
 			'body' => 'Logged out completely<br>'
 				. '<a href="'. get_page_uri($page) . '">'
 				. $page . '</a>'
 		);
 +	} else if ($pcmd === 'registration') {
 +		// registration
 +		$password_confirm = isset($_POST['password_confirm']) ? $_POST['password_confirm'] : '';
 +		$match_password = $password == $password_confirm;
 +		$usable_username = is_usable_username($username);
 +		if ($usable_username && $match_password) {
 +			if (create_user($username, $password, false)) {
 +			}
 +		}
 +		$action_url = get_base_uri() . '?plugin=loginform&pcmd=registration'
 +		. '&page=' . rawurlencode($page)
 +		. ($url_after_login ? '&url_after_login=' . rawurlencode($url_after_login) : '')
 +		. ($page_after_login ? '&page_after_login=' . rawurlencode($page_after_login) : '');
 +		ob_start();
 +?>
 +<style>
 +  .registrationformcontainer {
 +    text-align: center;
 +  }
 +  .registrationform table {
 +    margin-top: 1em;
 +	margin-left: auto;
 +	margin-right: auto;
 +  }
 +  .registrationform tbody td {
 +    padding: .5em;
 +  }
 +  .registrationform .label {
 +    text-align: right;
 +  }
 +  .registrationform .registration-button-container {
 +    text-align: right;
 +  }
 +  .registrationform .registrationbutton {
 +    margin-top: 1em;
 +  }
 +  .registrationform .errormessage {
 +    color: red;
 +  }
 +</style>
 +<div class="registrationformcontainer">
 +<form name="loginform" class="registrationform" action="<?php echo htmlsc($action_url) ?>" method="post">
 +<div>
 +<table style="border:0">
 +  <tbody>
 +  <tr>
 +    <td class="label"><label for="_plugin_registrationform_username"><?php echo htmlsc($_loginform_messages['username']) ?></label></td>
 +    <td><input type="text" name="username" value="<?php echo htmlsc($username) ?>" id="_plugin_registrationform_username" autoComplete="name"></td>
 +  </tr>
 +  <tr>
 +  <td class="label"><label for="_plugin_registrationform_form_password"><?php echo htmlsc($_loginform_messages['password']) ?></label></td>
 +  <td><input type="password" name="password" id="_plugin_registrationform_form_password" autoComplete="new-password"></td>
 +  </tr>
 +  <tr>
 +  <td class="label"><label for="_plugin_registrationform_password_confirm"><?php echo htmlsc($_loginform_messages['password_confirm']) ?></label></td>
 +  <td><input type="password" name="password_confirm" id="_plugin_registrationform_password_confirm"></td>
 +  </tr>
 +<?php if (!$usable_username): ?>
 +  <tr>
 +    <td></td>
 +    <td class="errormessage"><?php echo $_loginform_messages['unavailable_username'] ?></td>
 +  </tr>
 +<?php endif ?>
 +<?php if (!$match_password): ?>
 +  <tr>
 +    <td></td>
 +    <td class="errormessage"><?php echo $_loginform_messages['not_match_password'] ?></td>
 +  </tr>
 +<?php endif ?>
 +  <tr>
 +    <td></td>
 +    <td class="registration-button-container"><input type="submit" value="<?php echo htmlsc($_loginform_messages['register']) ?>" class="registrationbutton"></td>
 +  </tr>
 +  </tbody>
 +</table>
 +</div>
 +<div>
 +</div>
 +</form>
 +</div>
 +<script><!--
 +window.addEventListener && window.addEventListener("DOMContentLoaded", function() {
 +  var f = window.document.forms.loginform;
 +  if (f && f.username && f.password) {
 +    if (f.username.value) {
 +     f.password.focus && f.password.focus();
 +	} else {
 +     f.username.focus && f.username.focus();
 +	}
 +  }
 +});
 +//-->
 +</script>
 +<?php
 +		$body = ob_get_contents();
 +		ob_end_clean();
 +		return array(
 +			'msg' => $_loginform_messages['register'],
 +			'body' => $body,
 +			);
 +
 	} else {
 		// login
 +		$action_url = get_base_uri() . '?plugin=loginform'
 +		. '&page=' . rawurlencode($page)
 +		. ($url_after_login ? '&url_after_login=' . rawurlencode($url_after_login) : '')
 +		. ($page_after_login ? '&page_after_login=' . rawurlencode($page_after_login) : '');
 		ob_start();
 ?>
 <style>
   .loginformcontainer {
     text-align: center;
   }

:pukiwiki.ini.php|
私はUser definitionの近くに埋め込んでみました。
 /////////////////////////////////////////////////
 // User registration (0:Disable, 1:Enable)
 $user_registration = 1;

:.htaccess|
 # Prohibit direct access
 -<FilesMatch "\.(ini\.php|lng\.php|txt|gz|tgz|zip)$">
 +<FilesMatch "\.(ini\.php|lng\.php|txt|gz|tgz|zip|dat)$">
   Require all denied
 </FilesMatch>

ユーザー登録とはまた別だと思いますが、ユーザーのみ編集できる制限を実装してみました。

:ja.lng.php|
Page titlesの箇所へ追加
 $_title_cannotedit_not_login = 'ログインしていないため $1 は編集できません';
Skinの箇所へ追加
 $_LANG['skin']['registration'] = 'ユーザー登録';
差分
 ///////////////////////////////////////
 // loginform.inc.php
 $_loginform_messages = array(
 	'username' => 'ユーザー名:',
 	'password' => 'パスワード:',
 +	'password_confirm' => 'パスワード (確認):',
 	'login' => 'ログイン',
 +	'register' => 'ユーザー登録',
 -	'invalid_username_or_password' => 'ユーザー名またはパスワードが違います'
 +	'invalid_username_or_password' => 'ユーザー名またはパスワードが違います',
 +	'unavailable_username' => '既にそのユーザー名は使われています',
 +	'not_match_password' => 'パスワードが確認と一致しません'
 );

:skin/pukiwiki.skin.php|
  <?php if ($enable_login) { ?>
  	[
 -	<?php _navigator('login') ?>
 +	<?php _navigator('login') ?> |
 +	<?php _navigator('registration') ?>
 	]
  <?php } ?>

:lib/html.php|
 	$_LINK['upload']   = "$script?plugin=attach&amp;pcmd=upload&amp;page=$r_page";
 +	$_LINK['registration']   = "$script?cmd=loginform&amp;pcmd=registration&amp;page=$r_page";
 	$_LINK['canonical_url'] = $canonical_url;

:en.lng.php|
Page titlesの箇所へ追加
 $_title_cannotedit_not_login = '$1 is not editable because you are not log in';
Skinの箇所へ追加
 $_LANG['skin']['registration'] = 'User registration';
差分
 ///////////////////////////////////////
 // loginform.inc.php
 $_loginform_messages = array(
 	'username' => 'Username',
 	'password' => 'Password',
 +	'password_confirm' => 'Password (Confirm):',
	'login' => 'Log in',
 +	'register' => 'User registration',
 -	'invalid_username_or_password' => 'The username or password you entered is incorrect'
 +	'invalid_username_or_password' => 'The username or password you entered is incorrect',
 +	'unavailable_username' => 'The username is already in use',
 +	'not_match_password' => 'Password and confirmed password is not match'
 );

:plugin/edit.inc.php|
定数定義
 // ユーザーだけがページを編集できるようにする
 define('PLUGIN_EDIT_WRITE_USER_ONLY', FALSE);
plugin_edit_action関数
 function plugin_edit_action()
 {
 -	global $vars, $_title_edit;
 +	global $vars, $_title_edit, $auth_user, $_title_cannotedit_not_login;
 
 	if (PKWK_READONLY) die_message('PKWK_READONLY prohibits editing');
 
 	// Create initial pages
 	plugin_edit_setup_initial_pages();
 
 	$page = isset($vars['page']) ? $vars['page'] : '';
 	check_editable($page, true, true);
 	check_readable($page, true, true);
 	
 +	if (PLUGIN_EDIT_WRITE_USER_ONLY && !$auth_user) {
 +		$body = $title = str_replace('$1',
 +			htmlsc(strip_bracket($page)), $_title_cannotedit_not_login);
 +		$page = str_replace('$1', make_search($page), $_title_cannotedit_not_login);
 +		catbody($title, $page, $body);
 +		exit;
 +	}

''userlist.dat''に登録されたユーザーのデータが保存されます。

** 関連 [#fcca9a74]
- [[BugTrack/776]]

--------
- ユーザー管理をPukiWiki上で行うについては昔からいくつか試みられていて、 [[official:自作プラグイン/UserPageAdmin]], [[official:自作プラグイン/userauth.php]] のようなユーザープラグインがあります。 ですがリンク切れですね。。 PukiWiki単体でのユーザー管理の機能を発展させることはせず、 [[official:PukiWiki/Authentication]] にあるように、外部の認証システムとの連携を行う方向で機能追加しています。 LDAPやSAMLでの連携になります。こちらで行いたいことをできないでしょうか? -- [[umorigu]] &new{2022-01-10 (月) 18:57:54};
- 「利用者ページ」や「下書き」はユーザー管理とはまた別の機能ですね。これは別のBugTrackにしましょう -- [[umorigu]] &new{2022-01-10 (月) 19:00:21};
-- 「[[BugTrack/2554]] 利用者ページと下書き機能」として分離しました -- [[umorigu]] &new{2022-01-10 (月) 19:03:05};
- 「ユーザー登録システムの実装案 (はいふん)」はいふんさん、実装提案ありがとうございます。この件PukiWiki本体に組み込むのは少し難しい面があって、 userlist.dat はユーザーから絶対にアクセスできてはいけないのです。今の実装そのままだと、 /userist.dat を変更可能にする必要がありますし、Webサーバーの権限設定に気を付けないとWebからでもアクセス・ダウンロードできてしまいます。このようなファイルを置ける丁度いい場所があるかというとあまりなく、 wiki/ diff/ attach/ 等は配置するファイルが決まっていますし、 cache/ は比較的自由ですが、永続化するようなファイルを置くのには適切ではありません。 (その名の通り cache なので途中で消えても復元できるようなファイルを置くべき) このあたりを考えると、(Form自体は PukiWiki plugin の仕組みで用意してもいいのですが) userlist.dat はpukiWikiと無関係に、例えば /var/userdb/ のような WebサーバーのDocumentRoot と違う場所に置くようにして外部認証 [[pukiwiki:PukiWiki/Authentication/ExternalAuth]] として連携するのがいいんじゃないかな、と思っています。 -- [[umorigu]] &new{2022-03-18 (金) 23:58:02}; -- [[umorigu]] &new{2022-03-18 (金) 23:59:27};
-- 本命の対応は [[official:PukiWiki/Authentication]] にあるようにLDAPやSAMLでの外部の認証システム (パスワード変更などの基本的な機能がそろっているもの) との連携だと思っています。ただもっと簡易的に対応したいという話も理解できます。 -- [[umorigu]] &new{2022-03-19 (土) 00:04:40};
-- userlist.dat の内容と拡張子を pukiwiki.ini.php のようにPHPにすればすれば少なくとも直接ダウンロードされることはないのでは? さらに、デフォルトのパスをDATA_HOMEではなく安全な場所に設定するようコメントで促したり、とりあえずpukiwikiルート以下だったら警告を出す仕組みを実装したりして、あとは自己責任で導入させる手はあると思います。ファイル権限の問題は残りますが……しかしそう考えると、もともとPukiWiki自体が不適切な権限でも動いてしまう緩いポリシーではありますね。 -- [[-]] &new{2022-03-19 (土) 04:36:24};
-- いずれにしても、ユーザー管理をPukiWiki本体とは別建てにすべきなのはその通りだと思います。本件で要望されているユースケース的には、管理者はユーザーを積極的に管理したいわけではなく(誰でも登録できるのだし)、区別さえできれば(個別の下書きエリアさえ用意できれば)いいはずなので、たとえばExternalAuthによるソーシャルログイン連携が最適そうですが、やはり簡単に導入できるものではないですね。レンタルサーバーではできることも限られますし。 -- [[-]] &new{2022-03-19 (土) 05:24:12};
-- お二人ともご意見ありがとうございます。本体に組み込むのはやはり難しいのですね。umoriguさんのコメントを拝見してuserlist.datが懸念すべき場所に位置していることに気付きましたが、もしデータベースでユーザー管理を実装した場合では問題ないと思うのですが、実際どうなのでしょうか。 -- [[はいふん]] &new{2022-03-20 (日) 01:16:43};
-- 「もしデータベースでユーザー管理を実装した場合では問題ないと思うのですが」はい。その通りです。普通データベースと言った時にはMySQLやPostgreSQLのようなクライアント-サーバー型のものを指すことが多く「例えば /var/userdb/ のような WebサーバーのDocumentRoot と違う場所に置くように」と同じような形になります。DBでも例えば(ファイルベースの)SQLiteでファイル自体がWebから読み込み可能な場所にあると意味がありません。 -- [[umorigu]] &new{2022-03-21 (月) 16:55:23};
-- 「userlist.dat の内容と拡張子を pukiwiki.ini.php のようにPHPにすればすれば少なくとも直接ダウンロードされることはないのでは?」これはその通りですが、.phpファイルをWebから書き込み可能にすることの是非に話が移ります。もちろん、バグが無ければ問題ないのですが、セキュリティ的にはかなり、危うい対応ではあります。「もともとPukiWiki自体が不適切な権限でも動いてしまう緩いポリシーではありますね。」確かにこの面はあるのですが、もう20年ほど変わっていないポリシーなので、「今稼働しているサイトは適切な設定になっているはず」と期待するしかないですね。それに対して「新たに機能導入するためにポリシーを変える」というのはより慎重になるべきと思います。 -- [[umorigu]] &new{2022-03-21 (月) 17:02:30};
-- 参考までに、外部認証を使ってTwitterアカウントでログインできるようにする例を以前書きました。今は変わっているかもしれませんが https:// umorigu.hatenablog.com/entry/2017/05/07/062253 [[BugTrack/2421]] -- [[umorigu]] &new{2022-03-21 (月) 17:16:26};

#comment

トップ   編集 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Site admin: PukiWiki Development Team

PukiWiki 1.5.4+ © 2001-2022 PukiWiki Development Team. Powered by PHP 8.2.12. HTML convert time: 0.044 sec.

SourceForge