<?php
|
|
|
|
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
|
|
//
|
|
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
|
|
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
|
|
// $Id$
|
|
|
|
// This is for users to earn points in the community
|
|
// It's been implemented before and now it's being coded in v1.9.
|
|
// This code is provided here for you to check this implementation
|
|
// and make comments, please see
|
|
// http://tiki.org/tiki-index.php?page=ScoringSystemIdea
|
|
|
|
/**
|
|
*
|
|
*/
|
|
class ScoreLib extends TikiLib
|
|
{
|
|
const CACHE_KEY = 'score_events';
|
|
|
|
public function touch()
|
|
{
|
|
TikiLib::lib('cache')->invalidate(self::CACHE_KEY);
|
|
}
|
|
// User's general classification on site
|
|
/**
|
|
* @param $user
|
|
* @return mixed
|
|
*/
|
|
public function user_position($user)
|
|
{
|
|
global $prefs;
|
|
$score_expiry_days = $prefs['feature_score_expday'];
|
|
|
|
$score = $this->get_user_score($user);
|
|
|
|
if (empty($score_expiry_days)) {
|
|
// score does not expire
|
|
$query = "select count(*)+1 from `tiki_object_scores` tos
|
|
where `recipientObjectType`='user'
|
|
and `recipientObjectId`<> ?
|
|
and `pointsBalance` > ?
|
|
and tos.`id` = (select max(id) from `tiki_object_scores` where `recipientObjectId` = tos.`recipientObjectId` and `recipientObjectType`='user' group by `recipientObjectId`)
|
|
group by `recipientObjectId`";
|
|
|
|
$position = $this->getOne($query, [$user, $score]);
|
|
} else {
|
|
// score expires
|
|
$query = "select count(*)+1 from `tiki_object_scores` tos
|
|
where `recipientObjectType`='user'
|
|
and `recipientObjectId`<> ?
|
|
and `pointsBalance` - ifnull((select `pointsBalance` from `tiki_object_scores`
|
|
where `recipientObjectId`=tos.`recipientObjectId`
|
|
and `recipientObjectType`='user'
|
|
and `date` < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL ? DAY))
|
|
order by id desc limit 1), 0) > ?
|
|
and tos.`id` = (select max(id) from `tiki_object_scores` where `recipientObjectId` = tos.`recipientObjectId` and `recipientObjectType`='user' group by `recipientObjectId`)
|
|
group by `recipientObjectId`";
|
|
|
|
$position = $this->getOne($query, [$user, $score_expiry_days, $score]);
|
|
}
|
|
|
|
return $position;
|
|
}
|
|
|
|
// User's score on site
|
|
// allows getting score of a single user
|
|
/**
|
|
* @param $user
|
|
* @return mixed
|
|
*/
|
|
public function get_user_score($user, $dayLimit = 0)
|
|
{
|
|
global $prefs;
|
|
$score_expiry_days = $prefs['feature_score_expday'];
|
|
if (! empty($dayLimit) && $dayLimit < $score_expiry_days) {
|
|
//if the day limit is set, change the expiry to the day limit.
|
|
$score_expiry_days = $dayLimit;
|
|
}
|
|
$query = "select `pointsBalance` from `tiki_object_scores` where `recipientObjectId`=? and `recipientObjectType`='user' order by id desc";
|
|
$total_score = $this->getOne($query, [$user]);
|
|
if (empty($total_score)) {
|
|
$total_score = 0;
|
|
}
|
|
//if points don't expire, return total score; otherwise
|
|
if (empty($score_expiry_days)) {
|
|
return $total_score;
|
|
} else {
|
|
$query = "select `pointsBalance` from `tiki_object_scores`
|
|
where `recipientObjectId`=? and `recipientObjectType`='user' and
|
|
`date` < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL ? DAY))
|
|
order by id desc";
|
|
$score_at_expiry = $this->getOne($query, [$user, $score_expiry_days]);
|
|
if (empty($score_at_expiry)) {
|
|
$score_at_expiry = 0;
|
|
}
|
|
//subtract the score at expiry from the total score to get valid score
|
|
$score = $total_score - $score_at_expiry;
|
|
return $score;
|
|
}
|
|
}
|
|
|
|
// Number of users that go on ranking
|
|
/**
|
|
* @return mixed
|
|
*/
|
|
public function count_users()
|
|
{
|
|
global $prefs;
|
|
$score_expiry_days = $prefs['feature_score_expday'];
|
|
|
|
if (empty($score_expiry_days)) {
|
|
// score does not expire
|
|
$query = "select count(*) from `tiki_object_scores` tos
|
|
where `recipientObjectType`='user'
|
|
and `pointsBalance` > 0
|
|
and tos.`id` = (select max(id) from `tiki_object_scores` where `recipientObjectId` = tos.`recipientObjectId` and `recipientObjectType`='user' group by `recipientObjectId`)
|
|
group by `recipientObjectId`";
|
|
|
|
$count = $this->getOne($query, []);
|
|
} else {
|
|
// score expires
|
|
$query = "select count(*) from `tiki_object_scores` tos
|
|
where `recipientObjectType`='user'
|
|
and `pointsBalance` - ifnull((select `pointsBalance` from `tiki_object_scores`
|
|
where `recipientObjectId`=tos.`recipientObjectId`
|
|
and `recipientObjectType`='user'
|
|
and `date` < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL ? DAY))
|
|
order by id desc limit 1), 0) > 0
|
|
and tos.`id` = (select max(id) from `tiki_object_scores` where `recipientObjectId` = tos.`recipientObjectId` and `recipientObjectType`='user' group by `recipientObjectId`)
|
|
group by `recipientObjectId`";
|
|
|
|
$count = $this->getOne($query, [$score_expiry_days]);
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
// All event types, for administration
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function get_all_events()
|
|
{
|
|
$query = "SELECT * FROM `tiki_score` WHERE data IS NOT NULL";
|
|
$result = $this->query($query, []);
|
|
$event_list = [];
|
|
while ($res = $result->fetchRow()) {
|
|
$res['scores'] = json_decode($res['data']);
|
|
foreach ($res['scores'] as $key => $score) {
|
|
$res['scores'][$key]->validObjectIds = implode(",", $score->validObjectIds);
|
|
}
|
|
|
|
$event_list[] = $res;
|
|
}
|
|
return $event_list;
|
|
}
|
|
|
|
// Read information from admin and updates event's punctuation
|
|
/**
|
|
* @param $events
|
|
*/
|
|
public function update_events($events)
|
|
{
|
|
//clear old scores before re-inserting
|
|
$query = "delete from `tiki_score`";
|
|
$this->query($query);
|
|
|
|
foreach ($events as $event_name => $event_data) {
|
|
$reversalEvent = $event_data['reversalEvent'];
|
|
unset($event_data['reversalEvent']);
|
|
|
|
foreach ($event_data as $key => $rules) {
|
|
$tempArr = explode(',', $rules['validObjectIds']);
|
|
$event_data[$key]['validObjectIds'] = array_map('trim', $tempArr);
|
|
}
|
|
|
|
$event_data = json_encode($event_data);
|
|
|
|
$query = "insert into `tiki_score` (`event`,`reversalEvent`,`data`) values (?,?,?)";
|
|
$this->query($query, [$event_name, $reversalEvent, $event_data]);
|
|
}
|
|
$this->touch();
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Function to get available event types
|
|
*/
|
|
public function getEventTypes()
|
|
{
|
|
$graph = TikiLib::events()->getEventGraph();
|
|
sort($graph['nodes']);
|
|
return $graph['nodes'];
|
|
}
|
|
|
|
/**
|
|
* Bind events from the scoring system
|
|
* @param Tiki_Event_Manager $manager
|
|
*/
|
|
public function bindEvents($manager)
|
|
{
|
|
try {
|
|
$list = $this->getScoreEvents();
|
|
$eventsList = $list['events'];
|
|
$reversalEventsList = $list['reversalEvents'];
|
|
|
|
foreach ($reversalEventsList as $eventType) {
|
|
$manager->bind($eventType, Tiki_Event_Lib::defer('score', 'reversePoints'));
|
|
}
|
|
foreach ($eventsList as $eventType) {
|
|
$manager->bind($eventType, Tiki_Event_Lib::defer('score', 'assignPoints'));
|
|
}
|
|
} catch (TikiDb_Exception $e) {
|
|
// Prevent failures from locking-out users
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is the function called when a bound event is triggered. This stores the scoring transaction to the db
|
|
* and increases the score
|
|
* @param array $args
|
|
* @param string $eventType
|
|
* @throws Exception
|
|
*/
|
|
public function assignPoints($args = [], $eventType = "")
|
|
{
|
|
$rules = $this->getScoreEventRules($eventType);
|
|
$date = TikiLib::lib('tiki')->now;
|
|
//for each rule associated with the event, set up the scor
|
|
foreach ($rules as $rule) {
|
|
// if the object is invalid, do nothing.
|
|
if (! $this->objectIsValid($args, $rule)) {
|
|
continue;
|
|
}
|
|
$recipient = $this->evaluateExpression($rule->recipient, $args, "eval");
|
|
$recipientType = $this->evaluateExpression($rule->recipientType, $args);
|
|
$points = $this->evaluateExpression($rule->score, $args);
|
|
if (! $recipient || ! $points) {
|
|
continue;
|
|
}
|
|
if ($rule->expiration > 0 && ! $this->hasWaitedMinTime($args, $rule, $recipientType, $recipient)) {
|
|
continue;
|
|
}
|
|
//if user is anonymous, store a unique identifier in a cookie and set it as the user.
|
|
if (empty($args['user'])) {
|
|
$uniqueVal = getCookie('anonUserScoreId');
|
|
if (empty($uniqueVal)) {
|
|
$uniqueVal = getenv('HTTP_CLIENT_IP') . time() . rand();
|
|
$uniqueVal = md5($uniqueVal);
|
|
setCookieSection('anonUserScoreId', "anon" . $uniqueVal);
|
|
}
|
|
$args['user'] = $uniqueVal;
|
|
}
|
|
$pbalance = $this->getPointsBalance($recipientType, $recipient);
|
|
$data = [
|
|
'triggerObjectType' => $args['type'],
|
|
'triggerObjectId' => $args['object'],
|
|
'triggerUser' => $args['user'],
|
|
'triggerEvent' => $eventType,
|
|
'ruleId' => $rule->ruleId,
|
|
'recipientObjectType' => $recipientType,
|
|
'recipientObjectId' => $recipient,
|
|
'pointsAssigned' => $points,
|
|
'pointsBalance' => $pbalance + $points,
|
|
'date' => $date,
|
|
];
|
|
|
|
$id = $this->table()->insert($data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is the reversal function. If a reversal event is triggered, then check if there is an associated
|
|
* score and reverse it.
|
|
* @param $args
|
|
* @param $eventType
|
|
* @throws Exception
|
|
*/
|
|
public function reversePoints($args, $eventType)
|
|
{
|
|
$query = "SELECT event FROM `tiki_score` WHERE reversalEvent=?";
|
|
//if you find an original event, reverse it.
|
|
if ($originalEvent = $this->getOne($query, [$eventType])) {
|
|
//fetch all the scoring entries that were put in the last time
|
|
$date = $this->table()->fetchOne(
|
|
'date',
|
|
['triggerObjectType' => $args['type'],
|
|
'triggerObjectId' => $args['object'],
|
|
'triggerUser' => $args['user'],
|
|
'triggerEvent' => $originalEvent
|
|
],
|
|
["id" => "desc"]
|
|
);
|
|
$result = $this->table()->fetchAll(
|
|
['id', 'ruleId', 'pointsAssigned', 'recipientObjectType', 'recipientObjectId', 'reversalOf'],
|
|
['triggerObjectType' => $args['type'],
|
|
'triggerObjectId' => $args['object'],
|
|
'triggerUser' => $args['user'],
|
|
'triggerEvent' => $originalEvent,
|
|
'date' => $date,
|
|
]
|
|
);
|
|
|
|
$date = TikiLib::lib('tiki')->now;
|
|
foreach ($result as $row) {
|
|
// if the most recent transaction was a reversal, exit as to not reverse again
|
|
if ($row['reversalOf'] > 0) {
|
|
continue;
|
|
}
|
|
$pbalance = $this->getPointsBalance($row['recipientObjectType'], $row['recipientObjectId']);
|
|
$data = [
|
|
'triggerObjectType' => $args['type'],
|
|
'triggerObjectId' => $args['object'],
|
|
'triggerUser' => $args['user'],
|
|
'triggerEvent' => $eventType,
|
|
'ruleId' => $row['ruleId'],
|
|
'recipientObjectType' => $row['recipientObjectType'],
|
|
'recipientObjectId' => $row['recipientObjectId'],
|
|
'pointsAssigned' => -$row['pointsAssigned'],
|
|
'pointsBalance' => $pbalance - $row['pointsAssigned'],
|
|
'reversalOf' => $row['id'],
|
|
'date' => $date,
|
|
];
|
|
$id = $this->table()->insert($data);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
public function table($tableName = 'tiki_object_scores', $autoIncrement = true)
|
|
{
|
|
return TikiDb::get()->table($tableName);
|
|
}
|
|
|
|
/**
|
|
* This fetches all the events in the score table to bind all of them
|
|
* @return array
|
|
* @throws Exception
|
|
*/
|
|
private function getScoreEvents()
|
|
{
|
|
$cachelib = TikiLib::lib('cache');
|
|
if (! $result = $cachelib->getSerialized(self::CACHE_KEY)) {
|
|
$query = "SELECT * FROM `tiki_score` WHERE data IS NOT NULL";
|
|
$result = $this->query($query, []);
|
|
$event_list = [];
|
|
$event_reversal_list = [];
|
|
|
|
while ($res = $result->fetchRow()) {
|
|
$event_list[] = $res['event'];
|
|
if ($res['reversalEvent']) {
|
|
$event_reversal_list[] = $res['reversalEvent'];
|
|
}
|
|
}
|
|
$result = ['events' => $event_list,
|
|
'reversalEvents' => $event_reversal_list
|
|
];
|
|
$cachelib->cacheItem(self::CACHE_KEY, serialize($result));
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* This gets all the rules associated with a given event.
|
|
* @param $eventType
|
|
* @return mixed
|
|
*/
|
|
private function getScoreEventRules($eventType)
|
|
{
|
|
$query = "SELECT data FROM `tiki_score` WHERE event=? and data IS NOT NULL";
|
|
$result = $this->query($query, [$eventType]);
|
|
|
|
$rules = json_decode($result->fetchRow()['data']);
|
|
|
|
return $rules;
|
|
}
|
|
|
|
/**
|
|
* This retrieves the score of a given object.
|
|
* @param $recipientType
|
|
* @param $recipient
|
|
* @return bool|mixed
|
|
*/
|
|
public function getPointsBalance($recipientType, $recipient)
|
|
{
|
|
$query = "SELECT pointsBalance FROM `tiki_object_scores` WHERE recipientObjectType=? and recipientObjectId=? order by id desc";
|
|
$result = $this->getOne($query, [$recipientType,$recipient]);
|
|
|
|
if (empty($result)) {
|
|
return 0;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* This retrieves the score of a given object.
|
|
* @param $recipientType
|
|
* @param $recipient
|
|
* @return bool|mixed
|
|
*/
|
|
public function getGroupedPointsBalance($recipientType, $recipient)
|
|
{
|
|
$query = "SELECT ruleId, SUM(pointsAssigned) as 'points' FROM `tiki_object_scores` WHERE recipientObjectType=? and recipientObjectId=? group by ruleId";
|
|
$result = $this->fetchAll($query, [$recipientType,$recipient]);
|
|
|
|
if (empty($result)) {
|
|
return 0;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
public function getPointsBalanceForRuleId($recipientType, $recipient, $ruleId)
|
|
{
|
|
$query = "SELECT SUM(pointsAssigned) FROM `tiki_object_scores` WHERE recipientObjectType=? and recipientObjectId=? and ruleId=? group by ruleId";
|
|
$result = $this->getOne($query, [$recipientType,$recipient,$ruleId]);
|
|
|
|
if (empty($result)) {
|
|
return 0;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* This is only called and checked if you are assigning points. It is not done on reversals.
|
|
*
|
|
* @param $args
|
|
* @param $rule
|
|
* @return bool
|
|
*/
|
|
public function objectIsValid($args, $rule)
|
|
{
|
|
if (empty($rule->validObjectIds) || empty($rule->validObjectIds[0])) {
|
|
return true;
|
|
}
|
|
if (in_array($args['object'], $rule->validObjectIds) || in_array($args['type'] . ":" . $args['object'], $rule->validObjectIds)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* This is only called and checked if you are assigning points. It is not done on reversals.
|
|
*
|
|
* @param $args
|
|
* @param $rule
|
|
* @return bool
|
|
*/
|
|
public function hasWaitedMinTime($args, $rule, $recipientType, $recipient)
|
|
{
|
|
$query = "SELECT date FROM `tiki_object_scores`
|
|
WHERE triggerObjectType=? and triggerObjectId=? and ruleId=? and recipientObjectType=?
|
|
and recipientObjectId=? and reversalOf is null
|
|
order by id desc";
|
|
$date = $this->getOne($query, [$args['type'],$args['object'], $rule->ruleId, $recipientType, $recipient]);
|
|
$currentTime = time();
|
|
$expiration = $date + $rule->expiration;
|
|
if ($expiration > $currentTime) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* This is called to evaluate a given expression.
|
|
* @param $expr
|
|
* @param $args
|
|
* @param string $default
|
|
* @return bool|float|void
|
|
*/
|
|
public function evaluateExpression($expr, $args, $default = "str")
|
|
{
|
|
if (0 !== strpos($expr, "(")) {
|
|
$expr = "($default $expr)";
|
|
}
|
|
$runner = new Math_Formula_Runner(
|
|
[
|
|
'Math_Formula_Function_' => '',
|
|
'Tiki_Formula_Function_' => '',
|
|
]
|
|
);
|
|
try {
|
|
$runner->setVariables($args);
|
|
$runner->setFormula($expr);
|
|
return $runner->evaluate();
|
|
} catch (Math_Formula_Exception $e) {
|
|
return;
|
|
}
|
|
}
|
|
}
|