Reputation: 796
I'm almost there,but something is missing.My PHP app:
1)User is requesting to the server
2)The server is generating a long unique string and checks if exists in the DB:If YES then generate again(until it doesn't exists),if NO then add it to the DB and finish. All logic should be executed with a single request,i.e user should not request/refresh page if generated string exist.
I am stuck in the YES part.
My code (DISCLAIMER:I do not own parts of the following code)
<?php
class genPass
{
private $db;
function __construct() {
$this->db=new mysqli('localhost', 'user', 'pass', 'db');
$this->db->set_charset("utf8");
$this->db->autocommit(FALSE);
}
function __destruct() {
$this->db->close();
}
function isUsed($uid)
{
$stmt=$this->db->query("SELECT * FROM id WHERE udid='".$uid."'")or die($this->db->error);
while($stmt->num_rows <1) {
$newnum = $this->generateStrongPassword();
$newcheck=$this->db->query("SELECT * FROM id WHERE udid='".$newnum."'")or die($this->db->error);
if ($newcheck->num_rows >= 1) {
echo $newnum . " exists! \n"; <- WHAT TO DO IF EXISTS?WHICH PART OF THE SCRIPT SHOULD I RUN AGAIN
} else {
$this->db->query("INSERT INTO id (udid) VALUES ('".$newnum."')")or die($this->db->error);
echo "$newnum - CAN ISNERT@!@!@";
break;
}
}
}
public function generateStrongPassword($length = 3, $add_dashes = false, $available_sets = 'lu')
{
$sets = array();
if(strpos($available_sets, 'l') !== false)
$sets[] = 'ab';//'abcdefghjkmnpqrstuvwxyz';
if(strpos($available_sets, 'u') !== false)
$sets[] = 'AB';//'ABCDEFGHJKMNPQRSTUVWXYZ';
if(strpos($available_sets, 'd') !== false)
$sets[] = '23456789';
if(strpos($available_sets, 's') !== false)
$sets[] = '!@#$%&*?';
$all = '';
$password = '';
foreach($sets as $set)
{
$password .= $set[array_rand(str_split($set))];
$all .= $set;
}
$all = str_split($all);
for($i = 0; $i < $length - count($sets); $i++)
$password .= $all[array_rand($all)];
$password = str_shuffle($password);
if(!$add_dashes)
return $password;
$dash_len = floor(sqrt($length));
$dash_str = '';
while(strlen($password) > $dash_len)
{
$dash_str .= substr($password, 0, $dash_len) . '-';
$password = substr($password, $dash_len);
}
$dash_str .= $password;
return $this->$dash_str;
}
}
$obj = new genPass;
$ran=$obj->generateStrongPassword();
$obj->isUsed($ran);
?>
Upvotes: 1
Views: 4048
Reputation: 76405
Ok, I'll bite:
class GenPass
{
private $isUsedStmt = null;
private $db = null;
//constructor etc...
/**
* Works like most hashtables: re-hashes $id param until a unique hash is found
* uses sha256 algo, currently, no hash-collisions have been found, so pretty solid
* you could change to sha512, but that would be overkill
* @return string
**/
public function insertUniqueRandom($id = null)
{
$id = $id ? $id : $this->getRandomId();
do
{//use hashes, rehash in case of collision
$id = hash('256', $id);
}while($this->isUsed($id));
//once here, current $id hash has no collisions
$this->db->query('INSERT INTO `id` (udid) VALUES ("'.$id.'")');
return $id;//returns unique has that has been found & used/storred
}
/**
* Random string generator... returns random string
* of specfied $length (default 10)
* @param int $length = 10
* @return String
**/
public function getRandomId($length = 10)
{
$length = (int) ($length > 1 ? $length : 10);
$src = '0`12345~6789abc2%def&gh$ijklm!nopq?,rs><tuvwxyz';
$out ='';
for ($p = 0; $p < $length; $p++)
{
$char = $src{mt_rand(0, strlen($src))};
$out .= $p%2 ? strtoupper($char) : $char;
}
return $out;
}
/**
* Check if current hash already exists in DB, if so, return false, if not, return true
* @return Boolean
* @throws RuntimeException
**/
private function isUsed($uid)
{
$stmt = $this->getCheckUidStmt();
$stmt->bindParam('s', $uid);
if ($stmt->execute)
{
return $stmt->num_rows === 0 ? false : true;
}
throw new RuntimeException('failed to query for uid usage: '.$this->db->error);
}
/**
* Lazy-load isUsed's prepared statement
* The statement won't be prepared, unless the isUsed function is called
* @return \mysqli::prepare
**/
private function getCheckUidStmt()
{
if ($this->isUsedStmt === null)
{
$this->isUsedStmt = $this->db->prepare('SELECT udid FROM `id` WHERE udid = ?');
}
return $this->isUsedStmt;
}
}
This is, as the comments say, how most hashtables work anyway: hash a random value, if that hash is already being used, simply hash the duplicate hash again, until that hash is not being used anywhere.
Usage:
$gen = new GenPass;
$usedID = $gen->insertUniqueRandom();
echo $usedID, ' was just inserted';
$another = $gen->insertUniqueRandom('foobar');//works,
echo $another;//will echo:
//c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
$foobarAgain = $gen->insertUniqueRandom('foobar');
echo $foobarAgain;//foobar already existed, now this will echo:
//181cd581758421220b8c53d143563a027b476601f1a837ec85ee6f08c2a82cad
As you can see, trying to insert "foobar" twice will result in 2 unique id's. What's more, the length of a sha256 hash is a given: its 256 bits long, or 64 chars, so that makes it easy to store in a DB: VARCHAR(64)
is what you need... easy!
All things considered, I think it fair to say that this is probably the closest you'll get to a reliable, reasonably fast unique-random-id-generator you're going to get
Upvotes: 1
Reputation: 2856
You're using a function isUsed()
, which is good, but I'd suggest limiting that function to checking if the random key is used or not.
function isUsed($uid)
{
$stmt=$this->db->query("SELECT * FROM id WHERE udid='".$uid."'")or die($this->db->error);
if ($stmt->num_rows < 1) {
return FALSE;
} else {
// Already a duplicate key, so should return TRUE for sure!!!
return TRUE;
}
}
That way, you could use a loop to check:
while $obj->isUsed($ran) {
$ran = $obj->generateStrongPassword();
}
// put pwd in database after this loop!
By the way, this is just to explain the logic that has to be used... Check your code for further inconsistencies... :-)
Upvotes: 4