Initial commit
This commit is contained in:
commit
037ee17766
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016-2017 meta-tech.academy
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
99
README.md
Normal file
99
README.md
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
# PwsAuth
|
||||||
|
|
||||||
|
PwsAuth is an authentication protocol throught http header designed to web services
|
||||||
|
|
||||||
|
## Request Headers
|
||||||
|
|
||||||
|
request headers must be define as follow :
|
||||||
|
|
||||||
|
Pws-Authorization : $type $token
|
||||||
|
Pws-Ident : $userkey
|
||||||
|
|
||||||
|
the **$token** can be either a `loginToken` or a `sessionToken`
|
||||||
|
|
||||||
|
the **$token** is divided in four part
|
||||||
|
|
||||||
|
* a datetime formatted with the `Authenticator::DATE_FORMAT` format
|
||||||
|
* an obfuscate part 's token builded by date, common salt & the third token 's part
|
||||||
|
* a loginToken representing a user signed token for a specific login at given date
|
||||||
|
OR
|
||||||
|
a session token representing the session id
|
||||||
|
* noise data to be removed
|
||||||
|
|
||||||
|
the complete token is valid only if obfuscate part can be rebuild
|
||||||
|
this simple mecanism ensure that **sessionId** is valid and can be safety load
|
||||||
|
|
||||||
|
Authenticator 's configuration comes with a `hash.session.index` and `hash.noise.length` values
|
||||||
|
wich can be redefined to move the session token part into the complete token
|
||||||
|
|
||||||
|
<< hash.session.index >> << hash.noise.length >>
|
||||||
|
|-----------------------------------------------------------<<-^->>---------------------------------------------<<-^->>--------|
|
||||||
|
|- type -|-- date ---|------------ obfuscate token ---------<<-^->>-------------- session token ----------------<<-^->> noise -|
|
||||||
|
| | 1 | 2 | 3 | 4 |
|
||||||
|
PwsAuth2 242003031711e1a6104135f04c6c01e6cd5952ecafbb53c928603b0gb64tqo609qse6ovd7lhdvk4fnaqk7cdl26e4d4qh7jb41eu5f1zb5y79m8pgu3
|
||||||
|
|
||||||
|
|
||||||
|
### ClientSide
|
||||||
|
|
||||||
|
a request header can be generated via the `generateHeader($login, $key, $sessid=null)` method
|
||||||
|
the third parameter determine wich kind of token will be generated
|
||||||
|
|
||||||
|
### ServerSide
|
||||||
|
|
||||||
|
the Token can be retriew via the `getToken` method
|
||||||
|
|
||||||
|
`loginToken` is validate by the `check(Token $token, $login)` method
|
||||||
|
`loginToken` must match a public url with method `POST` and a couple of login/password
|
||||||
|
on successfull login, the session id must be transmit to the client.
|
||||||
|
|
||||||
|
`sessionToken` is valid only if the session can effectively be loaded, and the
|
||||||
|
user key match the given `Pws-Ident` value
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
configuration must be the same on server and client sides
|
||||||
|
hash definition is a convenient way to obfuscate your tokens
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
pwsauth :
|
||||||
|
|
||||||
|
type : PwsAuth2
|
||||||
|
|
||||||
|
header :
|
||||||
|
auth : Pws-Authorization
|
||||||
|
ident : Pws-Ident
|
||||||
|
|
||||||
|
salt :
|
||||||
|
common : jK5#p9Mh5.Zv}
|
||||||
|
# used for generating user specific salt
|
||||||
|
user.index : 10
|
||||||
|
user.length : 12
|
||||||
|
|
||||||
|
hash :
|
||||||
|
sep : /
|
||||||
|
algo : sha256
|
||||||
|
# effective token length size. out of bound data is simply noise
|
||||||
|
length : 52
|
||||||
|
# session index (or obfuscate length)
|
||||||
|
session.index : 58
|
||||||
|
# ending noise data length)
|
||||||
|
noise.length : 12
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authenticator instanciation
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
require_once(__dir__ . '/vendor/autoload.php');
|
||||||
|
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
use MetaTech\PwsAuth\Authenticator;
|
||||||
|
|
||||||
|
$config = Yaml::parse(file_get_contents(__dir__ . '/config/pwsauth.yml'));
|
||||||
|
$authenticator = new Authenticator($config['pwsauth']);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
a valid `$userkey` alone is useless
|
||||||
|
a valid `$sessionId` alone is useless
|
257
src/MetaTech/PwsAuth/Authenticator.php
Normal file
257
src/MetaTech/PwsAuth/Authenticator.php
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of the PwsAuth package.
|
||||||
|
*
|
||||||
|
* (c) meta-tech.academy
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
namespace MetaTech\PwsAuth;
|
||||||
|
|
||||||
|
use MetaTech\Util\Tool;
|
||||||
|
use MetaTech\PwsAuth\Token;
|
||||||
|
use MetaTech\PwsAuth\AuthenticateException;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* a simple class to authenticate access throught webservices using Pluie\Auth\Token
|
||||||
|
* and PwsAuth Protocol
|
||||||
|
*
|
||||||
|
* @package MetaTech\PwsAuth
|
||||||
|
* @class Authenticator
|
||||||
|
* @author a-Sansara
|
||||||
|
* @date 2016-05-02 13:08:01 CET
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class Authenticator
|
||||||
|
{
|
||||||
|
/*! @constant DATE_FORMAT */
|
||||||
|
const DATE_FORMAT = 'smHdiy';
|
||||||
|
/*! @constant DATE_LENGTH */
|
||||||
|
const DATE_LENGTH = 12;
|
||||||
|
/*! @constant DATE_LENGTH */
|
||||||
|
const DEFAULT_ALGO = 'sha256';
|
||||||
|
|
||||||
|
/*! @protected @var [assoc] $config */
|
||||||
|
protected $config;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @constructor
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public function __construct($config)
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* check if specified Token is a valid token
|
||||||
|
*
|
||||||
|
* @method isValid
|
||||||
|
* @public
|
||||||
|
* @param Pluie\Auth\Token $token
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isValid(Token $token)
|
||||||
|
{
|
||||||
|
return $token->getType() == $this->config['type'] && $this->checkObfuscatePart($token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* generate a unique signature at given time for specifyed user
|
||||||
|
*
|
||||||
|
* @method sign
|
||||||
|
* @public
|
||||||
|
* @param str $dtime given time in sqldatetime format
|
||||||
|
* @param str $login the user login
|
||||||
|
* @param str $key the user key
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
public function sign($dtime, $login, $key, $length=null)
|
||||||
|
{
|
||||||
|
$str = Tool::concat($this->config['hash']['sep'], [$dtime, $login, $this->getUserSalt($login), $key]);
|
||||||
|
return substr(hash($this->config['hash']['algo'], $str), is_null($length) ? - $this->config['hash']['length'] : - $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* generate the salt for a specific user
|
||||||
|
*
|
||||||
|
* @method getUserSalt
|
||||||
|
* @public
|
||||||
|
* @param str $login the user login
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
public function getUserSalt($login)
|
||||||
|
{
|
||||||
|
return substr(
|
||||||
|
hash(self::DEFAULT_ALGO, $login . $this->config['salt']['common']),
|
||||||
|
$this->config['salt']['user.index'],
|
||||||
|
$this->config['salt']['user.length']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* generate noise to obfuscate token
|
||||||
|
*
|
||||||
|
* @method obfuscate
|
||||||
|
* @orivate
|
||||||
|
* @param str $data
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
private function obfuscate($data, $date)
|
||||||
|
{
|
||||||
|
return substr(
|
||||||
|
hash(self::DEFAULT_ALGO, $date . $data . $this->config['salt']['common']),
|
||||||
|
- $this->config['hash']['session.index']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* check valid noise obfuscation
|
||||||
|
*
|
||||||
|
* @method checkObfuscatePart
|
||||||
|
* @public
|
||||||
|
* @param Pluie\Auth\Token $token
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function checkObfuscatePart(Token $token)
|
||||||
|
{
|
||||||
|
$tokenValue = $token->getValue();
|
||||||
|
return substr($tokenValue, 0, $this->config['hash']['session.index']) == $this->obfuscate($this->deobfuscate($tokenValue), $token->getDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* deoffuscate token
|
||||||
|
*
|
||||||
|
* @method deobfuscate
|
||||||
|
* @orivate
|
||||||
|
* @param str $data
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
private function deobfuscate($data)
|
||||||
|
{
|
||||||
|
return substr($data, $this->config['hash']['session.index']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @method getSessionId
|
||||||
|
* @orivate
|
||||||
|
* @param Pluie\Auth\Token $token
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
public function getSessionId(Token $token)
|
||||||
|
{
|
||||||
|
return $this->deobfuscate($token->getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* check validity of Token
|
||||||
|
*
|
||||||
|
* @mehtod check
|
||||||
|
* @public
|
||||||
|
* @param Pluie\Auth\Token $token
|
||||||
|
* @param str $login
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function check(Token $token, $login)
|
||||||
|
{
|
||||||
|
return !is_null($token) && $this->deobfuscate($token->getValue()) == $this->sign($token->getDate(), $login, $token->getIdent());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @method generateNoise
|
||||||
|
* @public
|
||||||
|
* @param str $data
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
public function generateNoise($data)
|
||||||
|
{
|
||||||
|
return substr(hash(self::DEFAULT_ALGO, str_shuffle($data)), - $this->config['hash']['noise.length']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @method generateToken
|
||||||
|
* @public
|
||||||
|
* @param str $login
|
||||||
|
* @param str $key
|
||||||
|
* @param str $sessid|null
|
||||||
|
* @return Pluie\Auth\Token
|
||||||
|
*/
|
||||||
|
public function generateToken($login, $key, $sessid=null)
|
||||||
|
{
|
||||||
|
$date = Tool::now();
|
||||||
|
$sessid = is_null($sessid) ? $this->sign($date, $login, $key) : $sessid;
|
||||||
|
$dt = Tool::formatDate($date, Tool::TIMESTAMP_SQLDATETIME, self::DATE_FORMAT);
|
||||||
|
$tokenValue = $dt . $this->obfuscate($sessid, $date) . $sessid;
|
||||||
|
$noise = $this->generateNoise($tokenValue);
|
||||||
|
return new Token($this->config['type'], $key, $date, $tokenValue, $noise);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @method generateHeader
|
||||||
|
* @public
|
||||||
|
* @param str $login
|
||||||
|
* @param str $key
|
||||||
|
* @param str $sessid
|
||||||
|
* @return []
|
||||||
|
*/
|
||||||
|
public function generateHeader($login, $key, $sessid=null)
|
||||||
|
{
|
||||||
|
$token = $this->generateToken($login, $key, $sessid);
|
||||||
|
return array(
|
||||||
|
$this->config['header']['auth'] .': ' . $token->getType() . ' ' . $token->getValue() . $token->getNoise(),
|
||||||
|
$this->config['header']['ident'].': ' . $token->getIdent()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* get token from specified $header or request headers.
|
||||||
|
*
|
||||||
|
* @method getToken
|
||||||
|
* @public
|
||||||
|
* @param [assoc] $headers
|
||||||
|
* @throw Pluie\Auth\AuthenticateException
|
||||||
|
* @return Pluie\Auth\Token
|
||||||
|
*/
|
||||||
|
public function getToken($headers = null)
|
||||||
|
{
|
||||||
|
$token = null;
|
||||||
|
try {
|
||||||
|
if (is_null($headers)) {
|
||||||
|
$headers = apache_request_headers();
|
||||||
|
}
|
||||||
|
$tokenValue = $headers[$this->config['header']['auth']] ?: '';
|
||||||
|
$ident = $headers[$this->config['header']['ident']] ?: '';
|
||||||
|
if (preg_match('/(?P<type>[a-z\d]+) (?P<date>\d{'.self::DATE_LENGTH.'})(?P<id>[a-z\d]+)/i', $tokenValue, $rs)) {
|
||||||
|
$date = Tool::formatDate($rs['date'], self::DATE_FORMAT, Tool::TIMESTAMP_SQLDATETIME);
|
||||||
|
$tokenValue = substr($rs['id'], 0, -$this->config['hash']['noise.length']);
|
||||||
|
$noise = substr($rs['id'], -$this->config['hash']['noise.length']);
|
||||||
|
$token = new Token($rs['type'], $ident, $date, $tokenValue, $noise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(\Exception $e) {
|
||||||
|
throw new AuthenticateException("invalid authentication protocol : ".$e->getMessage());
|
||||||
|
}
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* read header generate by generateHeader
|
||||||
|
*
|
||||||
|
* @method readHeader
|
||||||
|
* @public
|
||||||
|
* @param [str] $arrHeaders
|
||||||
|
* @return [assoc]
|
||||||
|
*/
|
||||||
|
public function readHeader($arrHeaders)
|
||||||
|
{
|
||||||
|
$headers = [];
|
||||||
|
foreach($arrHeaders as $h) {
|
||||||
|
$rs = preg_split('/:/', $h);
|
||||||
|
if (count($rs)==2) {
|
||||||
|
$headers[$rs[0]] = trim($rs[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $headers;
|
||||||
|
}
|
||||||
|
}
|
111
src/MetaTech/PwsAuth/Token.php
Normal file
111
src/MetaTech/PwsAuth/Token.php
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of the PwsAuth package.
|
||||||
|
*
|
||||||
|
* (c) meta-tech.academy
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
namespace MetaTech\PwsAuth;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* PwsAuth token
|
||||||
|
*
|
||||||
|
* @package MetaTech\PwsAuth
|
||||||
|
* @class Token
|
||||||
|
* @author a-Sansara
|
||||||
|
* @date 2016-05-02 13:16:01 CET
|
||||||
|
*/
|
||||||
|
class Token
|
||||||
|
{
|
||||||
|
/*! @protected @var $type */
|
||||||
|
protected $type = null;
|
||||||
|
/*! @protected @var $ident */
|
||||||
|
protected $ident = null;
|
||||||
|
/*! @protected @var $date */
|
||||||
|
protected $date = null;
|
||||||
|
/*! @protected @var $token */
|
||||||
|
protected $value = null;
|
||||||
|
/*! @protected @var $noise */
|
||||||
|
protected $noise = null;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @constructor
|
||||||
|
* @param str $type
|
||||||
|
* @param str $ident
|
||||||
|
* @param str $date
|
||||||
|
* @param str $value
|
||||||
|
* @param str $noise
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public function __construct($type, $ident, $date, $value, $noise)
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
$this->ident = $ident;
|
||||||
|
$this->date = $date;
|
||||||
|
$this->value = $value;
|
||||||
|
$this->noise = $noise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* desc
|
||||||
|
*
|
||||||
|
* @method getType
|
||||||
|
* @public
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
public function getType()
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* desc
|
||||||
|
*
|
||||||
|
* @method getIdent
|
||||||
|
* @public
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
public function getIdent()
|
||||||
|
{
|
||||||
|
return $this->ident;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* desc
|
||||||
|
*
|
||||||
|
* @method getDate
|
||||||
|
* @public
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
public function getDate()
|
||||||
|
{
|
||||||
|
return $this->date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* desc
|
||||||
|
*
|
||||||
|
* @method getValue
|
||||||
|
* @public
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
public function getValue()
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* desc
|
||||||
|
*
|
||||||
|
* @method getNoise
|
||||||
|
* @public
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
public function getNoise()
|
||||||
|
{
|
||||||
|
return $this->noise;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
81
src/MetaTech/Util/Tool.php
Normal file
81
src/MetaTech/Util/Tool.php
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of the PwsAuth package.
|
||||||
|
*
|
||||||
|
* (c) meta-tech.academy
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
namespace MetaTech\Util;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @package MetaTech\Util
|
||||||
|
* @class Tool
|
||||||
|
* @static
|
||||||
|
* @author a-Sansara
|
||||||
|
* @date 2014-12-11 17:46:29 CET
|
||||||
|
*/
|
||||||
|
class Tool
|
||||||
|
{
|
||||||
|
/*! @var TIMESTAMP_SQLDATETIME default sqldatetime timestamp */
|
||||||
|
const TIMESTAMP_SQLDATETIME = 'Y-m-d H:i:s';
|
||||||
|
/*! @var TIMESTAMP_SQLDATE default sqldate timestamp */
|
||||||
|
const TIMESTAMP_SQLDATE = 'Y-m-d';
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @constructor
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected function __construct()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @method now
|
||||||
|
* @public
|
||||||
|
* @static
|
||||||
|
* @param bool $full full format
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
public static function now($full = true)
|
||||||
|
{
|
||||||
|
return date($full ? self::TIMESTAMP_SQLDATETIME : self::TIMESTAMP_SQLDATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @method formatDate
|
||||||
|
* @public
|
||||||
|
* @static
|
||||||
|
* @param str $date
|
||||||
|
* @param str $fromFormat
|
||||||
|
* @param str $toFormat
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
public static function formatDate($date, $fromFormat='d-m-Y', $toFormat='Y-m-d')
|
||||||
|
{
|
||||||
|
$dt = \DateTime::createFromFormat($fromFormat, $date);
|
||||||
|
return !$dt ? null : $dt->format($toFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* concatenate various items in $list separate with specifyed separator $sep
|
||||||
|
*
|
||||||
|
* @method concat
|
||||||
|
* @public
|
||||||
|
* @param str $sep the used separator to concatenate items in $list
|
||||||
|
* @param [str] $list the list of items to concatenate
|
||||||
|
* @return str
|
||||||
|
*/
|
||||||
|
public static function concat($sep, $list)
|
||||||
|
{
|
||||||
|
$value = array_shift($list);
|
||||||
|
foreach ($list as $item) {
|
||||||
|
$value .= $sep . $item;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user