1 package Newslash::Model::Users;
2 use Newslash::Model::Base -base;
4 use Digest::MD5 qw(md5_hex md5_base64);
9 my ($self, $nickname, $passwd) = @_;
11 my $dbh = $self->connect_db;
12 my $sql = "SELECT * FROM users WHERE nickname = ?";
13 my $sth = $dbh->prepare($sql);
15 $sth->execute($nickname);
17 my $rs = $sth->fetchall_arrayref(+{});
27 if ($self->comparePassword($passwd, $u->{passwd}, $u->{uid}, 0, 0)) {
28 my $user = $self->_create_user_object($u);
36 return $self->select(uid => 1);
39 sub _create_user_object {
40 my ($self, $user) = @_;
45 for my $k (qw{uid nickname fakeemail homepage sig seclev matchname author}) {
46 $result->{$k} = $user->{$k};
48 # if seclev is greater than 10000, the user is admin
49 # if seclev is 0, the user is Anonymous user
50 $result->{admin} = $result->{seclev} >= 10000 ? 1 : 0;
51 $result->{login} = $result->{seclev} != 0 ? 1 : 0;
56 if (ref($user) eq 'ARRAY') {
57 my @ret = map { $_fn->($_) } @$user;
69 my $type = $params{target} || 'user';
71 # select from `users` table
72 if ($type eq 'user') {
74 if (exists $params{nickname}) {
75 my $u = $self->_select("users", nickname => $params{nickname});
76 $user = $u->[0] if $u;
78 elsif (exists $params{uid}) {
79 my $u = $self->_select("users", uid => $params{uid});
80 $user = $u->[0] if $u;
82 elsif (exists $params{matchname}) {
83 my $u = $self->_select("users", matchname => $params{matchname});
84 $user = $u->[0] if $u;
86 elsif (exists $params{email}) {
87 my $u = $self->_select("users", realemail => $params{email});
88 $user = $u->[0] if $u;
90 elsif (exists $params{author}) {
91 $user = $self->_select("users", author => $params{author});
95 return $self->_create_user_object($user);
100 # select from other table
102 if ($type eq 'acl') {
103 if (exists $params{uid}) {
104 $ret = $self->_select("users_acl", uid => $params{uid});
108 if ($type eq 'info') {
109 if (exists $params{uid}) {
110 $ret = $self->_select("users_info", uid => $params{uid});
114 if ($type eq 'comments') {
115 if (exists $params{uid}) {
116 $ret = $self->_select("users_comments", uid => $params{uid});
120 if ($type eq 'param') {
121 if (exists $params{uid}) {
122 $ret = $self->_select("users_param", uid => $params{uid});
130 my ($self, $table, $key, $value) = @_;
131 my $sql = "SELECT * FROM $table WHERE $key = ?";
132 my $dbh = $self->connect_db;
133 my $sth = $dbh->prepare($sql);
134 $sth->execute($value);
135 my $rs = $sth->fetchall_arrayref(+{});
141 #========================================================================
145 Returns salt value. This function exists because of historical reason.
155 =head2 comparePassword(PASSWD, MD5, ISPLAIN, ISENC)
157 (imported from Slash)
159 Given a password and an MD5 hex string, compares the two to see if they
160 represent the same value. To be precise:
162 If the password given is equal to the MD5 string, it must already be
163 in MD5 format and be correct, so return true
165 Otherwise, the password is assumed to be plaintext. Each possible
166 salt-encryption of it (including the encryption with empty salt) is
167 compared against the MD5 string. True is returned if there is any
170 If ISPLAIN is true, PASSWD is assumed to be plaintext, so the
171 (trivial equality) test against the encrypted MD5 is not performed.
173 If ISENC is true, PASSWD is assumed to be already encrypted, so the
174 tests of salting and encrypting it are not performed.
176 (If neither is true, all tests are performed. If both are true, no
177 tests are performed and 0 is returned.)
187 Possibly-correct password, either plaintext or already-MD5's,
192 Encrypted correct password.
204 sub comparePassword {
205 my ($self, $passwd, $md5, $uid, $is_plain, $is_enc) = @_;
208 return 1 if $passwd eq $md5;
211 # An old way of encrypting a user's password, which we have
212 # to check for reverse compatibility.
213 return 1 if md5_hex($passwd) eq $md5;
216 $salt_ar = [$self->_get_salt,];
218 # No? OK let's see if it matches any of the salts.
219 #my $slashdb = getCurrentDB();
220 #my $vu = $slashdb->{virtual_user};
221 #my $salt_ar = Slash::Apache::User::PasswordSalt::getPwSalts($vu);
222 #unshift @$salt_ar, ''; # always test the case of no salt
223 for my $salt (reverse @$salt_ar) {
224 # The current way of encrypting a user's password.
225 return 1 if md5_hex("$salt:$uid:$passwd") eq $md5;
226 # An older way, which we have to check for reverse
228 return 1 if length($salt) && md5_hex("$salt$passwd") eq $md5;
236 #========================================================================
238 =head2 _nickname_to_matchname
240 Convert nickname to matchname.
242 nickname consists of almost all alphabet, number, symbol.
244 matchname consists of lower alphabet and number.
248 sub _nickname_to_matchname {
249 my ($self, $nick) = @_;
251 $nick =~ s/[^a-zA-Z0-9]//g;
255 #========================================================================
257 =head2 random_password
259 (import from: changePassword)
261 Return new random 8-character password composed of 0..9, A..Z, a..z
262 (but not including possibly hard-to-read characters [0O1Iil]).
274 sub random_password {
275 my @chars = grep !/[0O1Iil]/, 0..9, 'A'..'Z', 'a'..'z';
276 return join '', map { $chars[rand @chars] } 0 .. 7;
279 #========================================================================
281 =head2 encrypt_password
283 (import from: encryptPassword)
285 Encrypts given password, using the most recent salt (if any) in
286 Slash::Apache::User::PasswordSalt for the current virtual user.
287 Currently uses MD5, but could change in the future, so do not
288 depend on the implementation.
298 Password to be encrypted.
310 sub encrypt_password {
311 my ($self, $passwd, $uid) = @_;
314 #my $slashdb = getCurrentDB();
315 #my $vu = $slashdb->{virtual_user};
316 #my $salt = Slash::Apache::User::PasswordSalt::getCurrentPwSalt($vu);
317 #$passwd = Encode::encode_utf8($passwd) if getCurrentStatic('utf8');
319 my $salt = $self->_get_salt;
321 return md5_hex("$salt:$uid:$passwd");
323 return md5_hex("$salt$passwd");
327 #========================================================================
343 use constant ERROR_BASE => 1000;
345 ERROR_INVALID_ADDRESS => ERROR_BASE + 10,
346 ERROR_NICKNAME_NOT_GIVEN => ERROR_BASE + 20,
347 ERROR_USER_EXISTS => ERROR_BASE + 30,
348 ERROR_INSERT_USER_FAILED => ERROR_BASE + 40,
349 ERROR_MAIL_REGISTERED => ERROR_BASE + 50,
350 ERROR_DO_SQL => ERROR_BASE + 50,
354 my ($self, $nickname, $email, $opts) = @_;
357 # check: nickname is given?
358 if ($nickname =~ m/\A\s+\z/) {
359 return { error => ERROR_NICKNAME_NOT_GIVEN };
362 my $matchname = $self->_nickname_to_matchname($nickname);
364 # check: email is valid?
365 if (!Email::Valid->address($email)) {
366 return { error => ERROR_INVALID_ADDRESS };
369 # check: nickname already exists?
370 my $user = $self->select(nickname => $nickname);
372 return { error => ERROR_USER_EXISTS };
375 # check: mail already registered?
376 $user = $self->select(email => $email);
378 return { error => ERROR_MAIL_REGISTERED };
382 my $dbh = $self->connect_db({AutoCommit => 0,});
386 # create user and set temporary password
387 my $passwd = $self->random_password;
388 my $enc_passwd = $self->encrypt_password($passwd, '');
390 INSERT INTO users (realemail, nickname, matchname, seclev, passwd)
391 VALUES (?, ?, ?, ?, ?)
393 $dbh->do($sql, undef, $email, $nickname, $matchname, 1, $enc_passwd);
394 my $uid = $dbh->last_insert_id(undef, undef, undef, undef);
399 return { error => ERROR_INSERT_USER_FAILED };
402 # create users_info, etc.
405 INSERT INTO users_info (uid, lastaccess, created_at, bio)
406 VALUES (?, NOW(), NOW(), ?)
408 $result = $dbh->do($sql, undef, $uid, '');
412 return { error => ERROR_DO_SQL };
415 for my $table (qw{users_prefs users_comments users_hits users_class}) {
416 $sql = "INSERT INTO $table (uid) VALUES (?)";
417 $result = $dbh->do($sql, undef, $uid);
421 return { error => ERROR_DO_SQL };
426 INSERT INTO users_index (uid, story_never_topic, slashboxes, story_always_topic)
429 $result = $dbh->do($sql, undef, $uid, '', '', '');
433 return { error => ERROR_DO_SQL };
440 return { error => 0, uid => $uid, password => $passwd };
443 =head2 clearRookie($uid, $time)
458 my ($self, $uid, $time) = @_;
459 return if !$uid || $uid == 1; # uid 1 is anonymous
461 my $dbh = $self->connect_db;
465 UPDATE users_class SET rookie = 0, rookie_cleared = NOW() WHERE uid = ?
467 my $rs = $dbh->do($sql, undef, $uid);
472 UPDATE users_class SET rookie = 0, rookie_cleared = ? WHERE uid = ?
474 my $rs = $dbh->do($sql, undef,$time, $uid);
480 #========================================================================
484 update user infomation.
500 my $uid = $params{uid};
505 if ($params{target} eq 'info') {
506 if (exists $params{add}) {
507 return $self->_update_users_info_add($uid, $params{field}, $params{add});
510 elsif ($params{target} eq 'class') {
511 if (exists $params{field} && exists $params{value}) {
512 return $self->_update_users_class($uid, $params{field}, $params{value});
514 elsif (exists $params{field} && $params{now}) {
515 return $self->_update_users_class_now($uid, $params{field});
521 sub _update_users_class_now {
522 my ($self, $uid, $column) = @_;
523 my $sql = "UPDATE users_class SET ? = NOW() WHERE uid = ?";
524 my $dbh = $self->connect_db();
525 my $rs = $dbh->do($sql, undef, $column, $uid);
533 sub _update_users_class {
534 my ($self, $uid, $column, $value) = @_;
535 my $sql = "UPDATE users_class SET ? = ? WHERE uid = ?";
536 my $dbh = $self->connect_db();
537 my $rs = $dbh->do($sql, undef, $column, $value, $uid);
545 sub _update_users_info_add {
546 my ($self, $uid, $field, $value) = @_;
548 my $sql = "UPDATE users_info SET $field = $field + ? WHERE uid = ?";
549 my $dbh = $self->connect_db();
550 my $rs = $dbh->do($sql, undef, $value, $uid);