2013-10-27 01:06:58 +02:00
< ? php
class User_AlreadyFavoritedError extends Exception {}
class User_NoInvites extends Exception {}
class User_HasNegativeRecord extends Exception {}
class User extends Rails\ActiveRecord\Base
{
static private $_current ;
public $current_email ;
public $country ;
public $passwordConfirmation ;
/**
* Set in ApplicationController .
*/
public $ip_addr ;
protected $post_count ;
static public function set_current_user ( User $user )
{
self :: $_current = $user ;
}
static public function current ()
{
return self :: $_current ;
}
# Defines various convenience methods for finding out the user's level
public function __call ( $method , $params )
{
if ( strpos ( $method , 'is_post_' )) {
return $this -> _parse_is_post_level_or ( $method );
} elseif ( strpos ( $method , 'is_' ) === 0 ) {
if ( is_int ( strpos ( $method , '_or_' )))
return $this -> parse_is_level_or ( $method );
else
return $this -> parse_is_level ( $method );
}
parent :: __call ( $method , $params );
}
public function log ( $ip )
{
2013-12-04 04:51:09 +01:00
return Rails :: cache () -> fetch ([ 'type' => 'user_logs' , 'id' => $this -> id , 'ip' => $ip ], [ 'expires_in' => '10 minutes' ], function () use ( $ip ) {
Rails :: cache () -> fetch ([ 'type' => 'user_logs' , 'id' => 'all' ], [ 'expires_in' => '1 day' ], function () {
2014-01-26 21:43:12 +01:00
# RP: Missing relation method "deleteAll()"
return UserLog :: where ( 'created_at < ?' , date ( 'Y-m-d 0:0:0' , strtotime ( '-3 days' ))) -> take () -> each ( 'destroy' );
2013-11-19 18:31:38 +01:00
});
2014-01-26 21:43:12 +01:00
# RP: Missing feature.
$log_entry = UserLog :: where ([ 'ip_addr' => $ip , 'user_id' => $this -> id ]) -> firstOrInitialize ();
2013-11-19 18:31:38 +01:00
$log_entry -> created_at = date ( 'Y-m-d H:i:s' );
2013-11-19 18:34:50 +01:00
return $log_entry -> save ();
2013-11-19 18:31:38 +01:00
});
2013-10-27 01:06:58 +02:00
}
# UserBlacklistMethods {
# TODO: I don't see the advantage of normalizing these. Since commas are illegal
# characters in tags, they can be used to separate lines (with whitespace separating
# tags). Denormalizing this into a field in users would save a SQL query.
public $blacklisted_tags ;
public function blacklisted_tags ()
{
return implode ( " \n " , $this -> blacklisted_tags_array ()) . " \n " ;
}
public function blacklisted_tags_array ()
{
if ( $this -> user_blacklisted_tag )
return preg_split ( " /( \r \n | \r | \n )/ " , trim ( $this -> user_blacklisted_tag -> tags ));
else
return [];
}
protected function _commit_blacklists ()
{
if ( $this -> user_blacklisted_tag && isset ( $this -> blacklisted_tags ))
$this -> user_blacklisted_tag -> updateAttribute ( 'tags' , $this -> blacklisted_tags );
}
protected function _set_default_blacklisted_tags ()
{
UserBlacklistedTag :: create ( array ( 'user_id' => $this -> id , 'tags' => implode ( " \r \n " , CONFIG () -> default_blacklists )));
}
# } UserAuthenticationMethods {
static public function authenticate ( $name , $pass )
{
return self :: authenticate_hash ( $name , self :: sha1 ( $pass ));
}
static public function authenticate_hash ( $name , $pass )
{
$user = parent :: where ( " lower(name) = lower(?) AND password_hash = ? " , $name , $pass ) -> first ();
return $user ;
}
static public function sha1 ( $pass )
{
return sha1 ( CONFIG () -> user_password_salt . '--' . $pass . '--' );
}
# } UserPasswordMethods {
public $password , $current_password ;
protected function validate_current_password ()
{
# First test to see if it's creating new user (no password_hash)
# or updating user. The second is to see if the action involves
# updating password (which requires this validation).
if ( $this -> password_hash and ( $this -> password or ( $this -> emailChanged () or $this -> current_email ))) {
if ( ! $this -> current_password )
$this -> errors () -> add ( 'current_password' , 'blank' );
elseif ( ! User :: authenticate ( $this -> name , $this -> current_password ))
$this -> errors () -> add ( 'current_password' , 'invalid' );
}
}
protected function _encrypt_password ()
{
if ( $this -> password )
$this -> password_hash = self :: sha1 ( $this -> password );
}
public function reset_password ()
{
$consonants = " bcdfghjklmnpqrstvqxyz " ;
$vowels = " aeiou " ;
$pass = " " ;
foreach ( range ( 1 , 4 ) as $i ) {
$pass .= substr ( $consonants , rand ( 0 , 20 ), 1 );
$pass .= substr ( $vowels , rand ( 0 , 4 ), 1 );
}
$pass .= rand ( 0 , 100 );
self :: connection () -> executeSql ( " UPDATE users SET password_hash = ? WHERE id = ? " , self :: sha1 ( $pass ), $this -> id );
return $pass ;
}
public function setPasswordConfirmation ( $value )
{
$this -> passwordConfirmation = $value ;
}
# } UserCountMethods {
# TODO: This isn't used anymore. Should be safe to delete.
static public function fast_count ()
{
return self :: connection () -> selectValue ( " SELECT row_count FROM table_data WHERE name = 'users' " );
}
protected function _increment_count ()
{
self :: connection () -> executeSql ( " UPDATE table_data set row_count = row_count + 1 where name = 'users' " );
}
protected function _decrement_count ()
{
self :: connection () -> executeSql ( " UPDATE table_data set row_count = row_count - 1 where name = 'users' " );
}
# } UserNameMethods {
static private function _find_name_helper ( $user_id )
{
if ( ! $user_id )
return CONFIG () -> default_guest_name ;
$user = self :: where ( 'id = ?' , $user_id ) -> first ();
if ( $user ) {
return $user -> name ;
} else {
return CONFIG () -> default_guest_name ;
}
}
static public function find_name ( $user_id )
{
2013-11-19 18:34:50 +01:00
return Rails :: cache () -> fetch ( 'user_name:' . $user_id , function () use ( $user_id ) {
2013-11-19 18:31:38 +01:00
try {
return self :: find ( $user_id ) -> name ;
} catch ( Rails\ActiveRecord\Exception\RecordNotFoundException $e ) {
return CONFIG () -> default_guest_name ;
}
});
2013-10-27 01:06:58 +02:00
}
static public function find_by_name ( $name )
{
return self :: where ( " lower(name) = lower(?) " , $name ) -> first ();
}
public function pretty_name ()
{
return str_replace ( '_' , ' ' , $this -> name );
}
// public function setPrettyName($value)
// {
// }
2013-11-19 18:31:38 +01:00
protected function update_cached_name ()
2013-10-27 01:06:58 +02:00
{
2013-11-19 18:31:38 +01:00
Rails :: cache () -> write ( " user_name: " . $this -> id , $this -> name );
2013-10-27 01:06:58 +02:00
}
# }
# UserApiMethods {
# iTODO:
public function toXml ( array $options = array ())
{
// !isset($options['indent']) && $options['indent'] = 2;
// if (isset($options['builder']))
// $xml = $options['builder'];
// else
// $xml = Builder::XmlMarkup.new('indent' => options[:indent])
// xml.post('name' => name, 'id' => id) do
// blacklisted_tags_array.each do |t|
// xml.blacklisted_tag('tag' => t)
// end
// yield options[:builder] if block_given?
// end
}
public function asJson ( array $args = array ())
{
return [ 'name' => $this -> name , 'blacklisted_tags' => $this -> blacklisted_tags_array (), 'id' => $this -> id ];
}
public function user_info_cookie ()
{
return implode ( ';' , [ $this -> id , $this -> level , ( $this -> use_browser ? " 1 " : " 0 " )]);
}
# }
public function find_by_name_nocase ( $name )
{
return User :: where ( " lower(name) = lower(?) " , $name ) -> first ();
}
# UserTagMethods {
# iTODO:
public function uploaded_tags ( array $options = array ())
{
$type = ! empty ( $options [ 'type' ]) ? $options [ 'type' ] : null ;
2013-11-19 18:31:38 +01:00
$uploaded_tags = Rails :: cache () -> read ( " uploaded_tags/ " . $this -> id . " / " . $type );
if ( $uploaded_tags ) {
return $uploaded_tags ;
}
2013-10-27 01:06:58 +02:00
2013-11-19 18:31:38 +01:00
if ( Rails :: env () == " test " ) {
# disable filtering in test mode to simplify tests
$popular_tags = " " ;
} else {
2013-10-27 01:06:58 +02:00
$popular_tags = implode ( ', ' , self :: connection () -> selectValues ( " SELECT id FROM tags WHERE tag_type = " . CONFIG () -> tag_types [ 'General' ] . " ORDER BY post_count DESC LIMIT 8 " ));
if ( $popular_tags )
$popular_tags = " AND pt.tag_id NOT IN ( ${ popular_tags } ) " ;
2013-11-19 18:31:38 +01:00
}
2013-10-27 01:06:58 +02:00
if ( $type ) {
$type = ( int ) $type ;
$sql = " SELECT
( SELECT name FROM tags WHERE id = pt . tag_id ) AS tag , COUNT ( * ) AS count
FROM posts_tags pt , tags t , posts p
WHERE p . user_id = { $this -> id }
AND p . id = pt . post_id
AND pt . tag_id = t . id
{ $popular_tags }
AND t . tag_type = { $type }
GROUP BY pt . tag_id
ORDER BY count DESC
LIMIT 6
" ;
} else {
$sql = " SELECT
( SELECT name FROM tags WHERE id = pt . tag_id ) AS tag , COUNT ( * ) AS count
FROM posts_tags pt , posts p
WHERE p . user_id = { $this -> id }
AND p . id = pt . post_id
$ { popular_tags }
GROUP BY pt . tag_id
ORDER BY count DESC
LIMIT 6
" ;
}
$uploaded_tags = self :: connection () -> select ( $sql );
2013-11-19 18:31:38 +01:00
Rails :: cache () -> write ( " uploaded_tags/ " . $this -> id . " / " . $type , $uploaded_tags , [ 'expires_in' => '1 day' ]);
2013-10-27 01:06:58 +02:00
return $uploaded_tags ;
}
public function voted_tags ( array $options = array ())
{
$type = ! empty ( $options [ 'type' ]) ? $options [ 'type' ] : null ;
2013-11-19 18:31:38 +01:00
$favorite_tags = Rails :: cache () -> read ( " favorite_tags/ " . $this -> id . " / " . $type );
if ( $favorite_tags ) {
return $favorite_tags ;
}
2013-10-27 01:06:58 +02:00
2013-11-19 18:31:38 +01:00
if ( Rails :: env () == " test " ) {
# disable filtering in test mode to simplify tests
$popular_tags = " " ;
} else {
2013-10-27 01:06:58 +02:00
$popular_tags = implode ( ', ' , self :: connection () -> selectValues ( " SELECT id FROM tags WHERE tag_type = " . CONFIG () -> tag_types [ 'General' ] . " ORDER BY post_count DESC LIMIT 8 " ));
if ( $popular_tags )
$popular_tags = " AND pt.tag_id NOT IN ( ${ popular_tags } ) " ;
2013-11-19 18:31:38 +01:00
}
2013-10-27 01:06:58 +02:00
if ( $type ) {
$type = ( int ) $type ;
$sql = " SELECT
( SELECT name FROM tags WHERE id = pt . tag_id ) AS tag , SUM ( v . score ) AS sum
FROM posts_tags pt , tags t , post_votes v
WHERE v . user_id = { $this -> id }
AND v . post_id = pt . post_id
AND pt . tag_id = t . id
{ $popular_tags }
AND t . tag_type = { $type }
GROUP BY pt . tag_id
ORDER BY sum DESC
LIMIT 6
" ;
} else {
$sql = " SELECT
( SELECT name FROM tags WHERE id = pt . tag_id ) AS tag , SUM ( v . score ) AS sum
FROM posts_tags pt , post_votes v
WHERE v . user_id = { $this -> id }
AND v . post_id = pt . post_id
$ { popular_tags }
GROUP BY pt . tag_id
ORDER BY sum DESC
LIMIT 6
" ;
}
$favorite_tags = self :: connection () -> select ( $sql );
2013-11-19 18:31:38 +01:00
Rails :: cache () -> write ( " favorite_tags/ " . $this -> id . " / " . $type , $favorite_tags , [ 'expires_in' => '1 day' ]);
2013-10-27 01:06:58 +02:00
return $favorite_tags ;
}
# }
# UserPostMethods {
public function recent_uploaded_posts ()
{
$posts = Post :: findBySql ( " SELECT p.* FROM posts p WHERE p.user_id = { $this -> id } AND p.status <> 'deleted' ORDER BY p.id DESC LIMIT 6 " );
return $posts ? : new Rails\ActiveRecord\Collection ();
}
public function recent_favorite_posts ()
{
return Post :: findBySql ( 'SELECT p.* FROM posts p JOIN post_votes pv ON p.id = pv.post_id WHERE pv.user_id = ' . $this -> id . ' AND pv.score = 3 ORDER BY pv.updated_at DESC LIMIT 6' );
}
public function favorite_post_count ( $options = array ())
{
return self :: connection () -> selectValue ( " SELECT COUNT(*) FROM post_votes v WHERE v.user_id = { $this -> id } AND v.score = 3 " );
}
public function post_count ()
{
if ( ! $this -> post_count )
$this -> post_count = Post :: where ( " user_id = ? AND status = 'active' " , $this -> id ) -> count ();
return $this -> post_count ;
}
public function held_post_count ()
{
2013-11-19 18:31:38 +01:00
$version = ( int ) Rails :: cache () -> read ( '$cache_version' );
$key = 'held-post-count/v=' . $version . '/u=' . $this -> id ;
return Rails :: cache () -> fetch ( $key , function () {
return Post :: where ([ 'user_id' => $this -> id , 'is_held' => true ]) -> where ( 'status <> ?' , 'deleted' ) -> count ();
});
2013-10-27 01:06:58 +02:00
}
# }
# UserLevelMethods {
public function pretty_level ()
{
return array_search ( $this -> level , CONFIG () -> user_levels );
}
protected function _set_role ()
{
if ( CONFIG () -> enable_account_email_activation )
$this -> level = CONFIG () -> user_levels [ " Unactivated " ];
else
$this -> level = CONFIG () -> starting_level ;
$this -> last_logged_in_at = date ( 'Y-m-d H:i:s' );
}
public function has_permission ( Rails\ActiveRecord\Base $record , $foreign_key = 'user_id' )
{
return ( $this -> is_mod_or_higher () || $record -> $foreign_key == $this -> id );
}
# Return true if this user can change the specified attribute.
#
# If record is an ActiveRecord object, return;s true if the change is allowed to complete.
#
# If record is an ActiveRecord class (eg. Pool rather than an actual pool), return;s
# false if the user would never be allowed to make this change for any instance of the
# object, and so the option should not be presented.
#
# For example, can_change(Pool, :description) return;s true (unless the user level
# is too low to change any pools), but can_change(Pool.find(1), :description) return;s
# false if that specific pool is locked.
#
# attribute usually corresponds with an actual attribute in the class, but any value
# can be used.
public function can_change ( Rails\ActiveRecord\Base $record , $attribute )
{
$method = " can_change_ " . $attribute ;
if ( $this -> is_mod_or_higher ())
return true ;
elseif ( method_exists ( $record , $method ))
return $record -> $method ( $this );
elseif ( method_exists ( $record , 'can_change' ))
2014-05-03 15:15:37 +02:00
return $record -> can_change ( $this , $attribute );
2013-10-27 01:06:58 +02:00
else
return true ;
}
static public function get_user_level ( $level )
{
static $user_level = [];
if ( ! $user_level ) {
foreach ( CONFIG () -> user_levels as $name => $value ) {
$normalized_name = strtolower ( str_replace ( ' ' , '_' , $name ));
$user_level [ $normalized_name ] = $value ;
}
}
return $user_level [ $level ];
}
# Created to statically get level name for level id.
static public function level_name ( $level_id )
{
return array_search ( $level_id , CONFIG () -> user_levels );
}
public function can_see_posts ()
{
return ! CONFIG () -> user_min_level_can_see_posts || $this -> level >= CONFIG () -> user_min_level_can_see_posts ;
}
# }
# module UserInviteMethods {
public function invite ( $name , $level )
{
if ( $this -> invite_count <= 0 ) {
throw new User_NoInvites ();
}
if (( int ) $level >= CONFIG () -> user_levels [ " Contributor " ])
$level = CONFIG () -> user_levels [ " Contributor " ];
$invitee = User :: where ([ 'name' => $name ]) -> first ();
if ( ! $invitee ) {
throw new Rails\ActiveRecord\Exception\RecordNotFoundException ();
}
if ( UserRecord :: where ( " user_id = ? AND is_positive = false AND reported_by IN (SELECT id FROM users WHERE level >= ?) " , $invitee -> id , CONFIG () -> user_levels [ " Mod " ]) -> exists () && ! $this -> is_admin ()) {
throw new User_HasNegativeRecord ();
}
// transaction do
if ( $level == CONFIG () -> user_levels [ " Contributor " ]) {
Post :: where ( " user_id = ? AND status = 'pending' " , $this -> id ) -> take () -> each ( function ( $post ) {
$post -> approve ( $id );
});
}
$invitee -> level = $level ;
$invitee -> invited_by = $this -> id ;
$invitee -> save ();
# iTODO: add support for decrement!
// decrement! :invite_count
self :: connection () -> executeSql ( " UPDATE users SET invite_count = invite_count - 1 WHERE id = " . $this -> id );
$this -> invite_count -- ;
// end
}
# }
# UserAvatarMethods {
# post_id is being destroyed. Clear avatar_post_ids for this post, so we won't use
# avatars from this post. We don't need to actually delete the image.
static public function clear_avatars ( $post_id )
{
self :: connection () -> executeSql ( " UPDATE users SET avatar_post_id = NULL WHERE avatar_post_id = ? " , $post_id );
}
public function avatar_url ()
{
return CONFIG () -> url_base . " /data/avatars/ " . $this -> id . " .jpg " ;
}
public function has_avatar ()
{
return ( bool ) $this -> avatar_post_id ;
}
public function avatar_path ()
{
return Rails :: root () . " /public/data/avatars/ " . $this -> id . " .jpg " ;
}
public function set_avatar ( $params )
{
$post = Post :: find ( $params [ 'id' ]);
if ( ! $post -> can_be_seen_by ( $this )) {
$this -> errors () -> add ( 'access' , " denied " );
return false ;
}
if ( $params [ 'top' ] < 0 or $params [ 'top' ] > 1 or
$params [ 'bottom' ] < 0 or $params [ 'bottom' ] > 1 or
$params [ 'left' ] < 0 or $params [ 'left' ] > 1 or
$params [ 'right' ] < 0 or $params [ 'right' ] > 1 or
$params [ 'top' ] >= $params [ 'bottom' ] or
$params [ 'left' ] >= $params [ 'right' ])
{
$this -> errors () -> add ( 'parameter' , " error " );
return false ;
}
$tempfile_path = Rails :: root () . " /public/data/ " . $this -> id . " .avatar.jpg " ;
$use_sample = $post -> has_sample ();
if ( $use_sample ) {
$image_path = $post -> sample_path ();
$image_ext = " jpg " ;
$size = $this -> _reduce_and_crop ( $post -> sample_width , $post -> sample_height , $params );
# If we're cropping from a very small region in the sample, use the full
# image instead, to get a higher quality image.
if (( $size [ 'crop_bottom' ] - $size [ 'crop_top' ] < CONFIG () -> avatar_max_height ) or
( $size [ 'crop_right' ] - $size [ 'crop_left' ] < CONFIG () -> avatar_max_width ))
$use_sample = false ;
}
if ( ! $use_sample ) {
$image_path = $post -> file_path ();
$image_ext = $post -> file_ext ;
$size = $this -> _reduce_and_crop ( $post -> width , $post -> height , $params );
}
try {
Moebooru\Resizer :: resize ( $image_ext , $image_path , $tempfile_path , $size , 95 );
} catch ( Moebooru\Exception\ResizeErrorException $x ) {
if ( file_exists ( $tempfile_path ))
unlink ( $tempfile_path );
$this -> errors () -> add ( " avatar " , " couldn't be generated ( " . $x -> getMessage () . " ) " );
return false ;
}
rename ( $tempfile_path , $this -> avatar_path ());
chmod ( $this -> avatar_path (), 0775 );
$this -> updateAttributes ( array (
'avatar_post_id' => $params [ 'post_id' ],
'avatar_top' => $params [ 'top' ],
'avatar_bottom' => $params [ 'bottom' ],
'avatar_left' => $params [ 'left' ],
'avatar_right' => $params [ 'right' ],
'avatar_width' => $size [ 'width' ],
'avatar_height' => $size [ 'height' ],
'avatar_timestamp' => date ( 'Y-m-d H:i:s' )
));
return true ;
}
private function _reduce_and_crop ( $image_width , $image_height , $params )
{
$cropped_image_width = $image_width * ( $params [ 'right' ] - $params [ 'left' ]);
$cropped_image_height = $image_height * ( $params [ 'bottom' ] - $params [ 'top' ]);
$size = Moebooru\Resizer :: reduce_to (
[ 'width' => $cropped_image_width , 'height' => $cropped_image_height ],
[ 'width' => CONFIG () -> avatar_max_width , 'height' => CONFIG () -> avatar_max_height ],
1 , true );
$size [ 'crop_top' ] = $image_height * $params [ 'top' ];
$size [ 'crop_bottom' ] = $image_height * $params [ 'bottom' ];
$size [ 'crop_left' ] = $image_width * $params [ 'left' ];
$size [ 'crop_right' ] = $image_width * $params [ 'right' ];
return $size ;
}
# }
# UserTagSubscriptionMethods {
// protected function tag_subscriptions_text_setter($text)
// {
// User.transaction do
// tag_subscriptions.clear
// text.scan(/\S+/).each do |new_tag_subscription|
// tag_subscriptions.create('tag_query' => new_tag_subscription) }
// end
// }
// def tag_subscriptions_text
// tag_subscriptions_text.map(&:tag_query).sort.join(" ")
// end
2014-01-12 13:05:06 +01:00
public function tag_subscription_posts ( $limit , $name )
{
return TagSubscription :: find_posts ( $this -> id , $name , $limit );
}
2013-10-27 01:06:58 +02:00
# }
# UserLanguageMethods {
protected function setSecondaryLanguageArray ( $langs )
{
$this -> secondary_languages = $langs ;
}
public function secondary_language_array ()
{
if ( ! is_array ( $this -> secondary_languages ))
$this -> secondary_languages = explode ( " , " , $this -> secondary_languages );
return $this -> secondary_languages ;
}
protected function _commit_secondary_languages ()
{
2013-12-06 11:13:03 +01:00
if ( ! $this -> secondary_languages )
return ;
2013-10-27 01:06:58 +02:00
2013-12-06 11:13:03 +01:00
if ( in_array ( " none " , $this -> secondary_languages ))
$this -> secondary_languages = " " ;
else
$this -> secondary_languages = implode ( " , " , $this -> secondary_languages );
2013-10-27 01:06:58 +02:00
}
# }
// $this->salt = CONFIG()->password_salt
// class << self
// attr_accessor :salt
// end
# For compatibility with AnonymousUser class
public function is_anonymous ()
{
return ! $this -> level ;
}
public function invited_by_name ()
{
return self :: find_name ( $this -> invited_by );
}
public function similar_users ()
{
# This uses a naive cosine distance formula that is very expensive to calculate.
# TODO: look into alternatives, like SVD.
$sql = "
SELECT
f0 . user_id as user_id ,
COUNT ( * ) / ( SELECT sqrt (( SELECT COUNT ( * ) FROM post_votes WHERE user_id = f0 . user_id ) * ( SELECT COUNT ( * ) FROM post_votes WHERE user_id = { $this -> id }))) AS similarity
FROM
vote v0 ,
vote v1 ,
users u
WHERE
v0 . post_id = v1 . post_id
AND v1 . user_id = { $this -> id }
AND v0 . user_id <> { $this -> id }
AND u . id = v0 . user_id
GROUP BY v0 . user_id
ORDER BY similarity DESC
LIMIT 6
" ;
return self :: connection () -> select ( $sql );
}
public function set_show_samples ()
{
$this -> show_samples = true ;
}
static public function generate_sql ( $params )
{
$query = self :: where ( 'true' );
if ( isset ( $params [ 'name' ]) && ( string ) $params [ 'name' ] !== '' ) {
$query -> where ( " name LIKE ? " , " % " . str_replace ( " " , " _ " , $params [ 'name' ]) . " % " );
}
if ( ! empty ( $params [ 'level' ]) && $params [ 'level' ] != " any " ) {
$query -> where ( " level = ? " , $params [ 'level' ]);
}
if ( ! empty ( $params [ 'id' ])) {
$query -> where ( " id = ? " , $params [ 'id' ]);
}
! isset ( $params [ 'order' ]) && $params [ 'order' ] = false ;
switch ( $params [ 'order' ]) {
case " name " :
$query -> order ( " lower(name) " );
break ;
case " posts " :
$query -> order ( " (SELECT count(*) FROM posts WHERE user_id = users.id) DESC " );
break ;
case " favorites " :
$query -> order ( " (SELECT count(*) FROM favorites WHERE user_id = users.id) DESC " );
break ;
case " notes " :
$query -> order ( " (SELECT count(*) FROM note_versions WHERE user_id = users.id) DESC " );
break ;
default :
$query -> order ( " id DESC " );
break ;
}
return $query ;
}
protected function associations ()
{
return array (
'has_one' => array (
'ban' => array ( 'foreign_key' => 'user_id' ),
'user_blacklisted_tag' => [ 'class_name' => 'UserBlacklistedTag' ]
),
'belongs_to' => array (
'avatar_post' => array ( 'class_name' => " Post " , 'foreign_key' => 'avatar_post_id' )
),
'has_many' => array (
'post_votes' => [ 'class_name' => 'PostVote' ],
'user_logs' => [ 'class_name' => 'UserLog' ],
// 'user_blacklisted_tags' => array('dependent' => 'delete_all'),
'tag_subscriptions' => array ( function () { $this -> order ( 'name' ); }, 'dependent' => 'delete_all' , 'class_name' => 'TagSubscription' )
)
);
}
protected function _can_signup ()
{
if ( ! CONFIG () -> enable_signups ) {
$this -> errors () -> add ( 'signups' , 'are disabled' );
return false ;
}
}
protected function callbacks ()
{
$before_create = array ( '_set_role' );
if ( CONFIG () -> show_samples )
$before_create [] = '_set_show_samples' ;
return array (
'before_validation_on_create' => [ '_can_signup' ],
'before_create' => $before_create ,
'before_save' => array ( '_encrypt_password' ),
'before_validation' => array ( '_commit_secondary_languages' ),
2013-11-19 18:31:38 +01:00
'after_save' => array ( '_commit_blacklists' , 'update_cached_name' ),
2013-10-27 01:06:58 +02:00
'after_create' => array ( '_set_default_blacklisted_tags' , '_increment_count' ),
'after_destroy' => array ( '_decrement_count' )
);
}
protected function validations ()
{
$validations = array (
'name' => array (
'length' => [ 'in' => [ 2 , 20 ], 'on' => 'create' ],
'format' => array ( 'with' => '/\A[^\s;,]+\Z/' , 'on' => 'create' , 'message' => 'cannot have whitespace, commas, or semicolons' ),
'uniqueness' => array ( true , 'on' => 'create' )
),
'password' => array (
'length' => array ( 'minimum' => 5 , 'if' => array ( 'property_exists' => 'password' )),
'confirmation' => true
),
'language' => array (
'format' => [ 'with' => '/^([a-z\-]+)|$/' ]
),
'secondary_languages' => array (
'format' => [ 'with' => '/^([a-z\-]+(,[a-z\0]+)*)?$/' ]
),
# Changing password requires current password.
'validate_current_password'
);
if ( CONFIG () -> enable_account_email_activation ) {
$validations [ 'email' ] = array (
'presence' => array ( true , 'on' => 'create' , 'if' => array ( 'property_exists' => 'email' ))
);
}
return $validations ;
}
protected function attrProtected ()
{
return [ 'level' , 'invite_count' ];
}
private function parse_is_level_or ( $method )
{
list ( $name , $operator ) = explode ( '_or_' , substr ( $method , 3 ));
$name = ucfirst ( $name );
$levels = CONFIG () -> user_levels ;
if ( ! isset ( $levels [ $name ]))
throw new InvalidArgumentException ( " User level name not found for " . $method );
$level = $levels [ $name ];
if ( $operator == 'higher' ) {
2014-01-12 16:27:17 +01:00
# For anonymous users
if ( ! $this -> id ) {
return false ;
} else {
return $this -> level >= $level ;
}
2013-10-27 01:06:58 +02:00
} elseif ( $operator == 'lower' ) {
2014-01-12 16:27:17 +01:00
# For anonymous users
if ( ! $this -> id ) {
return true ;
} else {
return $this -> level <= $level ;
}
2013-10-27 01:06:58 +02:00
} else {
throw new InvalidArgumentException ( " Invalid user level operator " . $operator );
}
}
private function parse_is_level ( $method )
{
2013-12-06 11:13:03 +01:00
# For anonymous users
if ( ! $this -> id ) {
return false ;
}
2013-10-27 01:06:58 +02:00
$level_name = ucfirst ( substr ( $method , 3 ));
$levels = CONFIG () -> user_levels ;
if ( ! isset ( $levels [ $level_name ])) {
throw new InvalidArgumentException ( " User level name not found for " . $method );
}
return $this -> level == $levels [ $level_name ];
}
}