how moodle1.9 save user attempt quiz result in database and which tables are updated when any quiz has been attempted by user?
Please guide me.
If possible please updated me, which functions are used to insert user quiz attempted data in moodle1.9 database?
From the attempt.php file (Moodle 1.9.7):
$attempt = quiz_create_attempt($quiz, $attemptnumber);
Then:
if (!$attempt->id = insert_record('quiz_attempts', $attempt)) {
error('Could not create new attempt');
}
From locallib.php:
/**
* Creates an object to represent a new attempt at a quiz
*
* Creates an attempt object to represent an attempt at the quiz by the current
* user starting at the current time. The ->id field is not set. The object is
* NOT written to the database.
* #return object The newly created attempt object.
* #param object $quiz The quiz to create an attempt for.
* #param integer $attemptnumber The sequence number for the attempt.
*/
function quiz_create_attempt($quiz, $attemptnumber) {
global $USER, $CFG;
if (!$attemptnumber > 1 or !$quiz->attemptonlast or !$attempt = get_record('quiz_attempts', 'quiz', $quiz->id, 'userid', $USER->id, 'attempt', $attemptnumber-1)) {
// we are not building on last attempt so create a new attempt
$attempt->quiz = $quiz->id;
$attempt->userid = $USER->id;
$attempt->preview = 0;
if ($quiz->shufflequestions) {
$attempt->layout = quiz_repaginate($quiz->questions, $quiz->questionsperpage, true);
} else {
$attempt->layout = $quiz->questions;
}
}
$timenow = time();
$attempt->attempt = $attemptnumber;
$attempt->sumgrades = 0.0;
$attempt->timestart = $timenow;
$attempt->timefinish = 0;
$attempt->timemodified = $timenow;
$attempt->uniqueid = question_new_attempt_uniqueid();
return $attempt;
}
Please refer to the source code for major details.
Related
I am working on an application that I need to post a secret variable. I wrote this code.
<form target="_blank" action="/validate/lista.php" method="POST">
<input type="hidden" name="evento" value="<?php echo $pname ?>" />
<button class="btn btn-block btn-md btn-outline-success">Lista</button>
</form>
My problem is that if the user inspect the element with chrome or whatever, he can see the value and change it before POST.
I could use SESSION but every user has a different session ID and this way I would need to POST the session ID (because they are separete applications), which I think is not secure. Or is it ok?
How can I prevent this? I am new to programming...
Thank you
Maintain HTML Form State safely ('Conversation' Tracking)
Keep track of the 'state' of an HTML Form as it is processed by the client and the server.
The typical 'conversation' is:
Send a new form to the client, often for a specific user who has to login.
The client enters data and returns it.
It is validated and may be sent out again.
The data changes are applied.
the client is informed of the result.
It sounds simple. Alas, we need to keep track of the 'state' of the form during the 'conversation'.
We need to record the state in a hidden field. This can open us up to various 'failure modes'.
This answer is one method of reliably keeping track of the 'conversations'.
Including people being 'malicious'. It happens. ;-/
This is a data change form so we don't want it applied to the wrong person.
There are various requirements:
Sensible ones:
prevent a form being processed twice
Ask a user to confirm the data if the form is too old
Malicious ones:
Changing the form to appear to be from a different user
Using an old copy of the form
Changing other hidden data to corrupt the user data
Now, we cannot prevent the client changing the hidden data, or storing it to replay later. etc.
What to do?
We need to ensure that if it is changed then we can detect that it is tampered with and tell the user about it. We do nothing.
If they send us an old stored valid copy then we can detect that as well.
Is there a simple way of doing this? Oh yes! :)
Answers:
Give each form a unique id: makes it easy to determine if we have already seen it.
Give each form a timestamp of when it was first created.
we can then decide the max age we allow to use it.
If it is too old then we just copy the entered data to a new form and ask the user to confirm it. see Captcha :)
When we process the form we store the form id.
The first check before processing a form is to see if we have already processed it
Identifying 'tampering'?
We encrypt it with AES! :) Only the server needs to know the password so there are no client issues.
If it is changed then the decrypt will fail and we just issue a new form to the user with the data input on it. :)
Is it a lot of code? Not really. And it makes forms processing safe.
One advantage is that has the protection for the CSRF attack built in so no separate code needed.
Program Code (FormState Class)
<?php
/**
* every 'data edit' form has one of these - without exeception.
*
* This ensures that the form I sent out came from me.
*
* It has:
* 1) A unique #id
* 2) A date time stamp and a lifetime
*
* Can be automatically generated and checked.
*/
class FormState {
const MAX_FORM_AGE = 600; // seconds
const ENC_PASSWORD = '327136823981d9e57652bba2acfdb1f2';
const ENC_IV = 'f9928260b550dbb2eecb6e10fcf630ba';
protected $state = array();
public function __construct($prevState = '')
{
if (!empty($prevState)) {
$this->reloadState($prevState); // will not be valid if fails
return;
}
$this->setNewForm();
}
/**
* Generate a new unique id and timestanp
*
* #param $name - optional name for the form
*/
public function setNewForm($name = '')
{
$this->state = array();
$this->state['formid'] = sha1(uniqid(true)); // each form has a unique id
$this->state['when'] = time();
if (!empty($name)) {
$this->setAttribute('name', $name);
}
}
/**
* retrieve attribute value
*
* #param $name attribute name to use
* #param $default value to return if attribute does not exist
*
* #return string / number
*/
public function getAttribute($name, $default = null)
{
if (isset($this->state[$name])) {
return $this->state[$name];
} else {
return $default;
}
}
/**
* store attribute value
*
* #param $name attribute name to use
* #param $value value to save
*/
public function setAttribute($name, $value)
{
$this->state[$name] = $value;
}
/**
* get the array
*/
public function getAllAttributes()
{
return $this->state;
}
/**
* the unique form id
*
* #return hex string
*/
public function getFormId()
{
return $this->getAttribute('formid');
}
/**
* Age of the form in seconds
* #return int seconds
*/
public function getAge()
{
if ($this->isValid()) {
return time() - $this->state['when'];
}
return 0;
}
/**
* check the age of the form
*
*#param $ageSeconds is age older than the supplied age
*/
public function isOutOfDate($ageSeconds = self::MAX_FORM_AGE)
{
return $this->getAge() >= $ageSeconds;
}
/**
* was a valid string passed when restoring it
* #return boolean
*/
public function isValid()
{
return is_array($this->state) && !empty($this->state);
}
/** -----------------------------------------------------------------------
* Encode as string - these are encrypted to ensure they are not tampered with
*/
public function asString()
{
$serialized = serialize($this->state);
$encrypted = $this->encrypt_decrypt('encrypt', $serialized);
$result = base64_encode($encrypted);
return $result;
}
/**
* Restore the saved attributes - it must be a valid string
*
* #Param $prevState
* #return array Attributes
*/
public function fromString($prevState)
{
$encrypted = #base64_decode($prevState);
if ($encrypted === false) {
return false;
}
$serialized = $this->encrypt_decrypt('decrypt', $encrypted);
if ($serialized === false) {
return false;
}
$object = #unserialize($serialized);
if ($object === false) {
return false;
}
if (!is_array($object)) {
throw new \Exception(__METHOD__ .' failed to return object: '. $object, 500);
}
return $object;
}
public function __toString()
{
return $this->asString();
}
/**
* Restore the previous state of the form
* will not be valid if not a valid string
*
* #param $prevState an encoded serialized array
* #return bool isValid or not
*/
public function reloadState($prevState)
{
$this->state = array();
$state = $this->fromString($prevState);
if ($state !== false) {
$this->state = $state;
}
return $this->isValid();
}
/**
* simple method to encrypt or decrypt a plain text string
* initialization vector(IV) has to be the same when encrypting and decrypting
*
* #param string $action: can be 'encrypt' or 'decrypt'
* #param string $string: string to encrypt or decrypt
*
* #return string
*/
public function encrypt_decrypt($action, $string)
{
$output = false;
$encrypt_method = "AES-256-CBC";
$secret_key = self::ENC_PASSWORD;
// iv - encrypt method AES-256-CBC expects 16 bytes - else you will get a warning
$secret_iv_len = openssl_cipher_iv_length($encrypt_method);
$secret_iv = substr(self::ENC_IV, 0, $secret_iv_len);
if ( $action == 'encrypt' ) {
$output = openssl_encrypt($string, $encrypt_method, $secret_key, OPENSSL_RAW_DATA, $secret_iv);
} else if( $action == 'decrypt' ) {
$output = openssl_decrypt($string, $encrypt_method, $secret_key, OPENSSL_RAW_DATA, $secret_iv);
}
if ($output === false) {
// throw new \Exception($action .' failed: '. $string, 500);
}
return $output;
}
}
Example Code
Full Example Application Source Code (Q49924789)
Website Using the supplied Source Code
FormState source code
Do we have an existing form?
$isExistingForm = !empty($_POST['formState']);
$selectedAction = 'start-NewForm'; // default action
if ($isExistingForm) { // restore state
$selectedAction = $_POST['selectedAction'];
$formState = new \FormState($_POST['formState']); // it may be invalid
if (!$formState->isValid() && $selectedAction !== 'start-NewForm') {
$selectedAction = "formState-isWrong"; // force user to start a new form
}
} else {
$_POST = array(); // yes, $_POST is just another PHP array
$formState = new \FormState();
}
Start New Form
$formState = new \FormState();
$_POST = array();
$displayMsg = "New formstate created. FormId: ". $formState->getFormId();
Store UserId (Database Id) in the FormState
$formState->setAttribute('userId' $userId);
Check a form being to old?
$secsToBeOutOfDate = 3;
if ($formState->isOutOfDate($secsToBeOutOfDate)) {
$errorMsg = 'Out Of Date Age: '. $secsToBeOutOfDate .'secs'
.', ActualAge: '. $formState->getAge();
}
Reload State from the form hidden field.
$formState = new \FormState('this is rubbish!!');
$errorMsg = "formState: isValid(): ". ($formState->isValid() ? 'True' : 'False');
Check if a form has already been processed.
if (isset($_SESSION['processedForms'][$formState->getFormId()])) {
$errorMsg = 'This form has already been processed. (' . $formState->getFormId() .')';
break;
}
$_SESSION['processedForms'][$formState->getFormId()] = true;
$displayMsg = "Form processed and added to list.";
I am trying to replicate table row and its relationship.
but I am getting error message that replicate() does not exist,
I have seen on stackoverflow that many have used replicate() without any issue, but i am getting this error
my controller code
public function copyshowtime($cinema_id,$show_date)
{
$date=new Carbon($show_date);
$current_show_date=$date->format('Y-m-d');
$next_show_date=$date->addDay()->format('Y-m-d');
$movieshowtime=Movies_showtimes::with('showdata')->where([['cinema_id','=',$cinema_id],['show_date','=',$current_show_date]])->get();
$newshowtime=$movieshowtime->replicate();
return $newshowtime;
}
Is there any namespace i have to use for using replicate() , I am unable to get solution from laravel website also.
help is appreciated.
You can use replicate() on a model but not on a collection.
By fetching your records using get() you are returning a collection.
If you are just expecting one record to be returned then replace get() with first() and then replicate() should exist as it will be returning an instance of the model rather than a collection:
public function copyshowtime($cinema_id,$show_date)
{
$date=new Carbon($show_date);
$current_show_date=$date->format('Y-m-d');
$next_show_date=$date->addDay()->format('Y-m-d');
$movieshowtime=Movies_showtimes::with('showdata')->where([['cinema_id','=',$cinema_id],['show_date','=',$current_show_date]])->first();
$newshowtime=$movieshowtime->replicate();
return $newshowtime;
}
You will also need to save() the $newshowtime.
This code worked perfectly for me
public function copyshowtime($cinema_id,$show_date)
{
$date=new Carbon($show_date);
$current_show_date=$date->format('Y-m-d');
$next_show_date=$date->addDay()->format('Y-m-d');
$movieshowtime=Movies_showtimes::with('showdata')->where([['cinema_id','=',$cinema_id],['show_date','=',$current_show_date]])->get();
foreach ($movieshowtime as $item)
{
$item->show_date=$next_show_date;
$item->show_id=NULL;
$newshowtime=$item->replicate();
$newshowtime->push();
foreach ($item->showdata as $sd)
{
$newshowdata = array(
'showdata_id' => NULL,
'show_id'=>$newshowtime->id,
'category_id'=>$sd->category_id,
'showdata_category'=>$sd->showdata_category,
'showdata_rate'=>$sd->showdata_rate
);
// print_r($newshowdata);
Movies_showdata::create($newshowdata);
}
}
return redirect()->back();
}
Any suggestions to improve this code will be appreciated.
This type of function would help to clone multiple records and add those records in the same table. I tried a similar code flow and worked.
/**
* Clone multiple records in same table
*
* #params int $cinemaId
* #params string $showDate
*
* #return bool $status
*
* #access public
*/
public function copyShowTime($cinemaId, $showDate)
{
$date = new Carbon($showDate);
$currentShowDate = $date->format('Y-m-d');
// Cloned & Create new records
$moviesShowTimeCollection = Movies_showtimes::with('showdata')->where([['cinema_id','=',$cinemaId],['show_date','=',$currentShowDate]])->get();
// Please check that Model name should change according to camelCases - Movies_showtimes to MoviesShowtimes
if(!$moviesShowTimeCollection->isEmpty()) {
$moviesShowTimeData = $moviesShowTimeCollection->toArray();
foreach ($moviesShowTimeData as $key => $value) {
$primaryKey = 'show_id'; // Needs to check the table primary key name
$primaryId = $value[$primaryKey];
$moviesShowTimeObj = Movies_showtimes::find($primaryId);
// below code can modify while cloaning
//$clonedMoviesShowTimeObj = $moviesShowTimeObj->replicate()->fill([
// 'column_name' => $updatedValue
//]);
$clonedMoviesShowTimeObj = $moviesShowTimeObj->replicate(); // just to clone a single record
$status = $clonedMoviesShowTimeObj->save();
}
}
}
Cheers!
You can easily replicate rows with new changes in that rows
$apcntReplicate = TrademarkApplicantMap::where('trademark_id', $trdIdForPostAssesment)->get();
foreach($apcntReplicate as $oldapnctdata)
{
$apcntreplicated = $oldapnctdata->replicate() ;
//update row data which will newly created by replicate
$apcntreplicated->row_name = $newrowdata;
//save new replicated row
$apcntreplicated->save();
}
Don't use toArray() then each element in the foreach loop will be an Eloquent object.
I have a php file(users.php) which I save the user info. Every time I update or add employee I need to open the file in text editor and make some changes. This is the sample lists of employees in $users array.
$users = array(
'001' => array('id'=>'001', 'name'=>'first lastname', 'dept'=>'Sales', 'position'=>'Lead Team', 'rate'=>'800', 'dayoff'=>'SUN'),
'002' => array('id'=>'002', 'name'=>'sec lastname', 'dept'=>'Sales', 'position'=>'Designer', 'rate'=>'800', 'dayoff'=>'SUN'),
'003' => array('id'=>'003', 'name'=>'david, sample', 'dept'=>'IT', 'position'=>'', 'rate'=>'220.83', 'dayoff'=>'SUN'),
'004' => array('id'=>'004', 'name'=>'Test, Johny', 'dept'=>'', 'position'=>'', 'rate'=>'600', 'dayoff'=>''),
'005' => array('id'=>'005', 'name'=>'Name, Last', 'dept'=>'IT', 'position'=>'Programmer', 'rate'=>'500', 'dayoff'=>'SUN')
);
When I compute their salary I grab all the details of employee($users array) from that file. This is my sample function.
function compute(){
global $users;
include('users.php');
//import list of users;
foreach($the_log as $k=>$v){
if($users[$k]){
//codes here
//show user data with computed salary
}
}
}
How can I make a simple database(like csv file or text file) not MySql or any open source database, so that I can add, edit and delete a user(with just a click) easily whenever I want. What I want to achieve here is to be able to make $users array editable. Is it possible?
Edit: When I use or save data in .csv file, How can I edit or delete a specific user/row?
Just because it's fun, I created an example of how you could do it.
Bare in mind, it's not tested so it might have some bugs but it shows how you could do it.
Since you got so much finished code, I'll leave that up to you to find the bugs. ;-) (However, if you find bugs, leave them as a comment and I'll update the answer).
Important note: Just like #deceze mentioned in his comment, this works well if you know that there won't be any simultaneous "connections" (several people working with the files at the same time) and that you always "open, do stuff, save" and not "open, do stuff, open in a new browser tab, do stuff, save in first tab, save in second tab". Otherwise, your first changes will be overwritten with your second changes and so on...
Class to manage users:
class Users
{
/**
* #var string
*/
protected $path;
/**
* #var array
*/
protected $users = [];
/**
* #param string $path Path to the user file (must be writeable)
*/
public function __construct($path)
{
$this->path = $path;
if (!is_file($this->path)) {
// The file doesn't exist yet, let's create it
file_put_contents($this->path, json_encode([]));
} else {
// It does exist. Load it.
$this->users = json_decode(file_get_contents($this->path), true);
}
}
/**
* Get all users
*
* #return array
*/
public function all()
{
return $this->users;
}
/**
* Get a specific user
*
* #param string|integer $userId The array index for that user
* #return array|null Returns null if user doesn't exist
*/
public function get($userId)
{
if (!array_key_exists($userId, $this->users)) {
// The key doesn't exist, return null
return null;
}
return $this->users[$userId];
}
/**
* Update or add a user
*
* #param string|integer $userId The array index for that user
* #param array $data The user info
* #return boolean
*/
public function save($userId, array $data)
{
$this->users[$userId] = $data;
$written = file_put_contents($this->path, json_encode($this->users));
return $written !== false;
}
}
How you would use it:
// When you have created the instance, use the same instance
// through out your whole application (only do: new Users() once).
// You could do this with some factory class.
$users = new Users('/path/to/users.json');
// List all users
foreach($users->all() as $userId => $row) {
echo $row['first_name'];
// ...
}
// Get user
$user = $users->get('001');
// Change user
$user['first_name'] = "Magnus";
// Save user (this is both update and add)
$users->save('001', $user);
So, this is my first encounter with SuiteCRM or any other CRM for that matter. I need to query the db on a table that is not used by the CRM for our quote system. So, I have created the module using module builder and modified the module file so that it uses the correct table. The problem is that when the query runs, SuiteCRM still adds its default where clauses and adds the deleted = 0 condition to the query.
So, I tried using the method that is described on this SO page. That doesn't work as I get the an error that I am using an undefined variable (db) and that I am calling to a member function fetchByAssoc() on a non-object. Now, I am placing the code in the moduleName.php file. Maybe that is my issue. I don't know as I have never worked on any other CRM project. If anyone can point me in the right direction as to what I will need to do to be able to query a different table other than the default CRM table and then show the results from that query inside of a dashlet, your help will be greatly appreciated.
I got the errors fixed. They were my fault as I had not referenced the object.
So, as requested, here is some of my code. This is my php file.
<?php
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
require_once('include/Dashlets/Dashlet.php');
class FrtwQuotesDashlet extends Dashlet {
var $height = '200'; // height of the dashlet
var $quoteData = "";
/**
* Constructor
*
* #global string current language
* #param guid $id id for the current dashlet (assigned from Home module)
* #param array $def options saved for this dashlet
*/
function FrtwQuotesDashlet($id, $def) {
$this->loadLanguage('FrtwQuotesDashlet');
if(!empty($def['height'])) // set a default height if none is set
$this->height = $def['height'];
parent::Dashlet($id); // call parent constructor
$this->isConfigurable = true; // dashlet is configurable
$this->hasScript = false; // dashlet has javascript attached to it
// if no custom title, use default
if(empty($def['title'])) $this->title = $this->dashletStrings['LBL_TITLE'];
else $this->title = $def['title'];
}
/**
* Displays the dashlet
*
* #return string html to display dashlet
*/
function display() {
$sql = "SELECT QuoteNbr, crmname, ShipToCity FROM quotes.quotehdr LIMIT 10";
$result = $GLOBALS["db"]->query($sql);
$quoteData = "<table>";
while($quotes = $GLOBALS["db"]->fetchByAssoc($result)){
foreach ($quotes as $quote) {
$quoteData .="<tr><td>".$quote[0]."</td><td>".$quote[1]."</td><td>".$quote[2]."</td></tr>";
}
}
$ss = new Sugar_Smarty();
//assign variables
//$ss->assign('greeting', $this->dashletStrings['LBL_GREETING']);
$ss->assign('quoteData', $this->quoteData);
$ss->assign('height', $this->height);
$str = $ss->fetch('custom/modules/Home/FrtwQuotesDashlet/FrtwQuotesDashlet.tpl');
return parent::display().$str;
}
}
?>
The issue was with the foreach loop. I removed it and now it works fine. In analyzing the code, the while loop actually does the iterations needed. So, by adding the foreach, what was happening was that the code was iterating over each row returned from the db and then doing some weird stuff -- as in, it would only return a partial string of what each value should be. Since I am querying on 3 fields, it would also loop over each row 3 times, thereby creating 3 different rows from each row. So, for anyone with similar issue, this is how working code looks.
<?php
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
require_once('include/Dashlets/Dashlet.php');
class FrtwQuotesDashlet extends Dashlet {
var $height = '200'; // height of the dashlet
var $quoteData = "";
/**
* Constructor
*
* #global string current language
* #param guid $id id for the current dashlet (assigned from Home module)
* #param array $def options saved for this dashlet
*/
function FrtwQuotesDashlet($id, $def) {
$this->loadLanguage('FrtwQuotesDashlet');
if(!empty($def['height']))
$this->height = $def['height'];
parent::Dashlet($id);
$this->isConfigurable = true;
$this->hasScript = false;
// if no custom title, use default
if(empty($def['title'])) $this->title = $this->dashletStrings['LBL_TITLE'];
else $this->title = $def['title'];
}
/**
* Displays the dashlet
*
* #return string html to display dashlet
*/
function display() {
$sql = "SELECT QuoteNbr, revnbr, crmname, ShipToCity FROM quotes.quotehdr LIMIT 10";
$result = $GLOBALS["db"]->query($sql);
$this->quoteData = "Need headers here when we determine exact fields....";
while($quotes = $GLOBALS["db"]->fetchByAssoc($result)){
$this->quoteData .="<tr><td width = \"30%\">".$quotes["QuoteNbr"].' '.$quotes['revnbr']."</td><td width = \"30%\">".$quotes["crmname"]."</td><td width = \"30%\">".$quotes["ShipToCity"]."</td></tr>";
}
$ss = new Sugar_Smarty();
//assign variables
// $ss->assign('greeting', $this->dashletStrings['LBL_GREETING']);
$ss->assign('greeting', "This is the Greeting....");
$ss->assign('quoteData', $this->quoteData);
$ss->assign('height', $this->height);
$str = $ss->fetch('modules/Home/Dashlets/FrtwQuotesDashlet/FrtwQuotesDashlet.tpl');
return parent::display().$str; // return parent::display for title and such
}
}
?>
I would like to make a PHP if condition code that will check if the last 10 articles or 10 minutes from the article reading by the user have already elapsed.
E.g.
A user open a page with id = 235 (this id value is in the url localhost/article/235 )
and this id value will be saved in session with a current timestamp and maybe his IP address
Then he read another article and the same will happen.
I need to remember the clicked stuff for another ten clicks and then reset that only for the first row. E.g. after the 10th click the id and timestamp will not became 11th row but will replace the 1st row in the list.
The php condition in CodeIgniter will then check these values and will update the article hit counter value in the articles table and column counter like this:
$this->db->where('id', $id);
$this->db->set('counter', 'counter+1', FALSE);
$this->db->update('articles');
But before calling this code I need to make this check from the session?
How to do that?
I think storing e.g. 10 entries in the session with timestamps per user will be enough.
Just don't save the same page in the session twice.
And the condition will check the current timestamp with the saved one and if it is more than e.g. 10 minutes or the user have read/clicked another 10 articles it will allow the update counter php code.
I don't need to have this bulletproof. Just to disable the increment using browser's refresh button.
So, if he wants to increment the counter he will need to wait ten minutes or read another 10 articles ;)
You should definitely go for Sessions. It saves you bandwidth consumption and is much easier to handle. Unless, of course, you need the data on the client-side, which, by your explanation, I assume you don't. Assuming you went for sessions, all you gotta do is store an array with the data you have. The following code should do it:
$aClicks = $this->session
->userdata('article_clicks');
// Initialize the array, if it's not already initialized
if ($aClicks == false) {
$aClicks = array();
}
// Now, we clean our array for the articles that have been clicked longer than
// 10 minutes ago.
$aClicks = array_filter(
$aClicks,
function($click) {
return (time() - $click['time']) < 600; // Less than 10 minutes elapsed
}
);
// We check if the article clicked is already in the list
$found = false;
foreach ($aClicks as $click) {
if ($click['article'] === $id) { // Assuming $id holds the article id
$found = true;
break;
}
}
// If it's not, we add it
if (!$found) {
$aClicks[] = array(
'article' => $id, // Assuming $id holds the article id
'time' => time()
);
}
// Store the clicks back to the session
$this->session
->set_userdata('article_clicks', $aClicks);
// If we meet all conditions
if (count($aClicks) < 10) {
// Do something
}
I assumne that $clicks is an array with up to ten visited articles. The id is used as key and the timestamp as value. $id is the id of the new article.
$clicks = $this->session->userdata('article_clicks');
//default value
$clicks = ($clicks)? $clicks : array();
//could be loaded from config
$maxItemCount = 10;
$timwToLive= 600;
//helpers
$time = time();
$deadline = $time - $timeToLive;
//add if not in list
if(! isset($clicks[$id]) ){
$clicks[$id] = $time;
}
//remove old values
$clicks = array_filter($clicks, function($value){ $value >= $deadline;});
//sort newest to oldest
arsort($clicks);
//limit items, oldest will be removed first because we sorted the array
$clicks = array_slice($clicks, 0, $maxItemCount);
//save to session
$this->session->>set_userdata('article_clicks',$clicks)
Usage:
//print how mch time has passed since the last visit
if(isset($clicks[$id]){
echo "visited ".($time-$clicks[$id]). "seconds ago." ;
} else {
echo "first visit";
}
EDIT: you have to use arsort not rsort or the keys will be lost, sorry
Based on Raphael_ code and your question you can try this:
<?php
$aClicks = $this->session
->userdata('article_clicks');
$nextId = $this->session->userdata('nextId');
// Initialize the array, if it's not already initialized
if ($aClicks == false) {
$aClicks = array();
$nextId = 0;
}
// Now, we clean our array for the articles that have been clicked longer than
// 10 minutes ago.
$aClicks = array_filter($aClicks, function($click) {
return (time() - $click['time']) < 600; // Less than 10 minutes elapsed
}
);
// We check if the article clicked is already in the list
$found = false;
foreach ($aClicks as $click) {
if ($click['article'] === $id) { // Assuming $id holds the article id
$found = true;
break;
}
}
// If it's not, we add it
if (!$found) {
$aClicks[$nextId] = array(
'article' => $id, // Assuming $id holds the article id
'time' => time()
);
$nextId++;
$this->session->set_userdata('nextId', $nextId);
}
$this->session->set_userdata('article_clicks', $aClicks);
if (count($aClicks) > 10 && $nextId > 9) {
$this->session->set_userdata('nextId', 0);
echo "OK!";
}
?>
I hope I understood correctly what you need.
Usage:
$this->load->library('click');
$this->click->add($id, time());
The class API is very simple and the code is commented. You can also check if an item expired(), if exists() and you can get() item saved time.
Remember that:
Each item will expire after 10 minutes (see $ttl)
Only 10 items are saved in session (see $max_entries)
class Click
{
/**
* CI instance
* #var object
*/
private $CI;
/**
* Click data holder
* #var array
*/
protected $clicks = array();
/**
* Time until an entry will expire
* #var int
*/
protected $ttl = 600;
/**
* How much entries do we store ?
* #var int
*/
protected $max_entries = 10;
// -------------------------------------------------------------------------
public function __construct()
{
$this->CI =& get_instance();
if (!class_exists('CI_Session')) {
$this->CI->load->library('session');
}
// load existing data from user's session
$this->fetch();
}
// -------------------------------------------------------------------------
/**
* Add a new page
*
* #access public
* #param int $id Page ID
* #param int $time Added time (optional)
* #return bool
*/
public function add($id, $time = null)
{
// If page ID does not exist and limit has been reached, stop here
if (!$this->exist($id) AND (count($this->clicks) == $this->max_entries)) {
return false;
}
$time = !is_null($time) ? $time : time();
if ($this->expired($id)) {
$this->clicks[$id] = $time;
return true;
}
return false;
}
/**
* Get specified page ID data
*
* #access public
* #param int $id Page ID
* #return int|bool Added time or `false` on error
*/
public function get($id)
{
return ($this->exist($id)) ? $this->clicks[$id] : false;
}
/**
* Check if specified page ID exists
*
* #access public
* #param int $id Page ID
* #return bool
*/
public function exist($id)
{
return isset($this->clicks[$id]);
}
/**
* Check if specified page ID expired
*
* #access public
* #param int $id Page ID
* #return bool
*/
public function expired($id)
{
// id does not exist, return `true` so it can added
if (!$this->exist($id)) {
return true;
}
return ((time() - $this->clicks[$id]) >= $this->ttl) ? true : false;
}
/**
* Store current clicks data in session
*
* #access public
* #return object Click
*/
public function save()
{
$this->CI->session->set_userdata('article_clicks', serialize($this->clicks));
return $this;
}
/**
* Load data from user's session
*
* #access public
* #return object Click
*/
public function fetch()
{
if ($data = $this->CI->session->userdata('article_clicks')) {
$this->clicks = unserialize($data);
}
return $this;
}
public function __destruct()
{
$this->save();
}
}
You could easily wrap that into a class of it's own that serializes the information into a string and that is able to manipulate the data, e.g. to add another value while taking care to cap at the maximum of ten elements.
A potential usage could look like, let's assume the cookie last would contain 256 at start:
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(10), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(20), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(30), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(40), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(50), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(60), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(70), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(80), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(90), "\n";
echo $_COOKIE['last'] = (new StringQueue($_COOKIE['last']))->add(100), "\n";
And the output (Demo):
10,256
20,10,256
30,20,10,256
40,30,20,10,256
50,40,30,20,10,256
60,50,40,30,20,10,256
70,60,50,40,30,20,10,256
80,70,60,50,40,30,20,10,256
90,80,70,60,50,40,30,20,10,256
100,90,80,70,60,50,40,30,20,10
A rough implementation of that:
class StringQueue implements Countable
{
private $size = 10;
private $separator = ',';
private $values;
public function __construct($string) {
$this->values = $this->parseString($string);
}
private function parseString($string) {
$values = explode($this->separator, $string, $this->size + 1);
if (isset($values[$this->size])) {
unset($values[$this->size]);
}
return $values;
}
public function add($value) {
$this->values = $this->parseString($value . $this->separator . $this);
return $this;
}
public function __toString() {
return implode(',', $this->values);
}
public function count() {
return count($this->values);
}
}
It's just some basic string operations, here with implode and explode.