Initial version
This commit is contained in:
commit
4673a7a133
28 changed files with 2610 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*~
|
||||
.*.swp
|
6
README.txt
Normal file
6
README.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
SMS Gateway Application
|
||||
=======================
|
||||
|
||||
Could be use with "My SMS Gateway" Android app :
|
||||
|
||||
https://play.google.com/store/apps/details?id=fr.nope.smsgateway
|
1
data/.gitignore
vendored
Normal file
1
data/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
app.log
|
82
data/mysql.init.sql
Normal file
82
data/mysql.init.sql
Normal file
|
@ -0,0 +1,82 @@
|
|||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
--
|
||||
-- Database: `smsgw`
|
||||
--
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `incoming_msg`
|
||||
--
|
||||
|
||||
CREATE TABLE `incoming_msg` (
|
||||
`uuid` varchar(40) NOT NULL,
|
||||
`number` text NOT NULL,
|
||||
`text` text NOT NULL,
|
||||
`timestampms` int(11) NOT NULL,
|
||||
`status` varchar(20) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `outgoing_msg`
|
||||
--
|
||||
|
||||
CREATE TABLE `outgoing_msg` (
|
||||
`datetime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`uuid` varchar(40) NOT NULL,
|
||||
`number` text NOT NULL,
|
||||
`text` text NOT NULL,
|
||||
`uniqueid` bigint(11) UNSIGNED NOT NULL,
|
||||
`nbfrag` int(11) NOT NULL,
|
||||
`status` varchar(30) NOT NULL DEFAULT 'pending',
|
||||
`lastupdate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `outgoing_msg_frag`
|
||||
--
|
||||
|
||||
CREATE TABLE `outgoing_msg_frag` (
|
||||
`msguid` bigint(11) UNSIGNED NOT NULL,
|
||||
`fragid` int(11) NOT NULL,
|
||||
`status` varchar(20) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
--
|
||||
-- Indexes for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- Indexes for table `incoming_msg`
|
||||
--
|
||||
ALTER TABLE `incoming_msg`
|
||||
ADD PRIMARY KEY (`uuid`);
|
||||
|
||||
--
|
||||
-- Indexes for table `outgoing_msg`
|
||||
--
|
||||
ALTER TABLE `outgoing_msg`
|
||||
ADD PRIMARY KEY (`uuid`),
|
||||
ADD UNIQUE KEY `uniqueid` (`uniqueid`);
|
||||
|
||||
--
|
||||
-- Indexes for table `outgoing_msg_frag`
|
||||
--
|
||||
ALTER TABLE `outgoing_msg_frag`
|
||||
ADD PRIMARY KEY (`fragid`,`msguid`);
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
1
includes/.gitignore
vendored
Normal file
1
includes/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
config.local.php
|
96
includes/config.inc.php
Normal file
96
includes/config.inc.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
// Log configuration
|
||||
|
||||
// Log file path
|
||||
$log_file=$root_dir_path.'/data/app.log';
|
||||
|
||||
// Log level (DEBUG / INFO / WARNING / ERROR)
|
||||
$log_level='INFO';
|
||||
|
||||
// Enable debug mode (on screen)
|
||||
$debug=False;
|
||||
|
||||
// SMS Gateway
|
||||
$smsgw_url="http://192.168.8.28:8080";
|
||||
$smsgw_ssl_verify=true;
|
||||
|
||||
// Database
|
||||
|
||||
// Example for MySQL :
|
||||
$db_dsn="mysql:host=localhost;dbname=smsgw";
|
||||
$db_user="smsgw";
|
||||
$db_pwd="smsgw";
|
||||
$db_options=array();
|
||||
|
||||
// FluentPDO path
|
||||
$fluentpdo_path="FluentPDO/FluentPDO.php";
|
||||
|
||||
/*
|
||||
* Config Mail
|
||||
*/
|
||||
|
||||
$phpmail_path="Mail.php";
|
||||
|
||||
/*
|
||||
* Méthode d'envoie :
|
||||
* - mail : envoie avec la méthode PHP mail()
|
||||
* - sendmail : envoie la commande sendmail du système
|
||||
* - smtp : envoie en utilisant un serveur SMTP
|
||||
*/
|
||||
$mail_send_method='smtp';
|
||||
|
||||
/*
|
||||
* Paramètres d'envoie :
|
||||
* Ces paramètres dépende de la méthode utilisé. Repporté vous à la documentation
|
||||
* de PEAR :: Mail pour plus d'information.
|
||||
* Lien : http://pear.php.net/manual/en/package.mail.mail.factory.php
|
||||
* Infos :
|
||||
* List of parameter for the backends
|
||||
* mail
|
||||
* o If safe mode is disabled, $params will be passed as the fifth
|
||||
* argument to the PHP mail() function. If $params is an array,
|
||||
* its elements will be joined as a space-delimited string.
|
||||
* sendmail
|
||||
* o $params["sendmail_path"] - The location of the sendmail program
|
||||
* on the filesystem. Default is /usr/bin/sendmail.
|
||||
* o $params["sendmail_args"] - Additional parameters to pass to the
|
||||
* sendmail. Default is -i.
|
||||
* smtp
|
||||
* o $params["host"] - The server to connect. Default is localhost.
|
||||
* o $params["port"] - The port to connect. Default is 25.
|
||||
* o $params["auth"] - Whether or not to use SMTP authentication.
|
||||
* Default is FALSE.
|
||||
* o $params["username"] - The username to use for SMTP authentication.
|
||||
* o $params["password"] - The password to use for SMTP authentication.
|
||||
* o $params["localhost"] - The value to give when sending EHLO or HELO.
|
||||
* Default is localhost
|
||||
* o $params["timeout"] - The SMTP connection timeout.
|
||||
* Default is NULL (no timeout).
|
||||
* o $params["verp"] - Whether to use VERP or not. Default is FALSE.
|
||||
* o $params["debug"] - Whether to enable SMTP debug mode or not.
|
||||
* Default is FALSE.
|
||||
* o $params["persist"] - Indicates whether or not the SMTP connection
|
||||
* should persist over multiple calls to the send() method.
|
||||
*/
|
||||
$mail_send_params = NULL;
|
||||
|
||||
/*
|
||||
* Headers :
|
||||
*/
|
||||
$mail_hearders = array(
|
||||
'MIME-Version' => '1.0',
|
||||
'Content-Type' => 'text/plain; charset=UTF-8; format=flowed',
|
||||
'Content-Transfer-Encoding' => '8bit',
|
||||
);
|
||||
|
||||
// Mail sender address
|
||||
$mail_sender='sms-noreply@example.fr';
|
||||
|
||||
// Catch all email to the following specified address
|
||||
//$mail_catch='root@example.fr';
|
||||
|
||||
// Load local configuration file is present
|
||||
if (is_file($root_dir_path.'/includes/config.local.php')) {
|
||||
require $root_dir_path.'/includes/config.local.php';
|
||||
}
|
25
includes/core.php
Normal file
25
includes/core.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
|
||||
|
||||
// Root directory path
|
||||
$root_dir_path=realpath(dirname(__FILE__).'/../');
|
||||
|
||||
// Include App's includes and vendor directories to PHP include paths
|
||||
set_include_path($root_dir_path.'/includes' . PATH_SEPARATOR . $root_dir_path.'/vendor' . PATH_SEPARATOR . get_include_path());
|
||||
|
||||
|
||||
require_once('config.inc.php');
|
||||
|
||||
// Check $public_root_url end
|
||||
if (substr($public_root_url, -1)=='/') {
|
||||
$public_root_url=substr($public_root_url, 0, -1);
|
||||
}
|
||||
|
||||
require_once('logging.php');
|
||||
require_once('functions.php');
|
||||
|
||||
require_once('sms_gw_api.php');
|
||||
$smsgw = new sms_gw_api($smsgw_url, $smsgw_ssl_verify);
|
||||
|
||||
require('db.php');
|
302
includes/db.php
Normal file
302
includes/db.php
Normal file
|
@ -0,0 +1,302 @@
|
|||
<?php
|
||||
|
||||
try {
|
||||
require $fluentpdo_path;
|
||||
$pdo = new PDO($db_dsn,$db_user,$db_pwd,$db_options);
|
||||
$fpdo = new FluentPDO($pdo);
|
||||
$fpdo -> debug = function ($q) {
|
||||
$time = sprintf('%0.3f', $q->getTime() * 1000) . ' ms';
|
||||
$rows = ($q->getResult()) ? $q->getResult()->rowCount() : 0;
|
||||
$query = $q->getQuery();
|
||||
$msg = "# DB query ($time; rows = $rows) : $query";
|
||||
|
||||
$parameters = $q->getParameters();
|
||||
if ($parameters) {
|
||||
if (is_array($parameters)) {
|
||||
$msg .= "\n# Parameters: '" . implode("', '", $parameters) . "'";
|
||||
}
|
||||
else {
|
||||
$msg .= "\n# Parameters: '" . varDump($parameters) . "'";
|
||||
}
|
||||
}
|
||||
|
||||
logging('DEBUG',$msg);
|
||||
};
|
||||
}
|
||||
catch(Exception $e) {
|
||||
logging('ERROR',"Fail to connect to DB (DSN : '$db_dsn') : ".$e->getMessage());
|
||||
fatal_error("Impossible de se connecter à la base de données");
|
||||
}
|
||||
|
||||
function db_now() {
|
||||
// 1970-01-01 00:00:01
|
||||
return date('Y-m-d G:i:s');
|
||||
}
|
||||
|
||||
function get_incoming_msg($uuid) {
|
||||
global $fpdo;
|
||||
$result = $fpdo -> from('incoming_msg')
|
||||
-> where('uuid=?', $uuid)
|
||||
-> execute();
|
||||
|
||||
if ($result !== false) {
|
||||
return $result -> fetch();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function create_incoming_msg($number, $text, $timestampms) {
|
||||
global $fpdo;
|
||||
|
||||
$uuid = generate_uuid();
|
||||
$values = array (
|
||||
'uuid' => $uuid,
|
||||
'number' => $number,
|
||||
'text' => $text,
|
||||
'timestampms' => $timestampms,
|
||||
);
|
||||
$result = $fpdo -> insertInto('incoming_msg',$values)
|
||||
-> execute();
|
||||
if ($result === false)
|
||||
return -1;
|
||||
|
||||
return $uuid;
|
||||
}
|
||||
|
||||
function get_outgoing_msg($uuid, $status=false) {
|
||||
global $fpdo;
|
||||
$where=array();
|
||||
if ($uuid)
|
||||
$where['uuid']=$uuid;
|
||||
if ($status)
|
||||
$where['status']=$status;
|
||||
if (empty($where))
|
||||
return -1;
|
||||
$result = $fpdo -> from('outgoing_msg')
|
||||
-> where($where)
|
||||
-> execute();
|
||||
|
||||
if ($result !== false) {
|
||||
return $result -> fetch();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function get_outgoing_msg_by_uniqueid($uniqueid) {
|
||||
global $fpdo;
|
||||
$where=array('uniqueid' => $uniqueid);
|
||||
$result = $fpdo -> from('outgoing_msg')
|
||||
-> where($where)
|
||||
-> execute();
|
||||
|
||||
if ($result !== false) {
|
||||
return $result -> fetch();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function create_outgoing_msg($number, $text) {
|
||||
global $fpdo;
|
||||
|
||||
$uuid = generate_uuid();
|
||||
$values = array (
|
||||
'uuid' => $uuid,
|
||||
'number' => $number,
|
||||
'text' => $text,
|
||||
);
|
||||
$result = $fpdo -> insertInto('outgoing_msg',$values)
|
||||
-> execute();
|
||||
if ($result === false)
|
||||
return -1;
|
||||
|
||||
return $uuid;
|
||||
}
|
||||
|
||||
function handle_outgoing_msg($msg) {
|
||||
if (!is_array($msg))
|
||||
$msg=get_outgoing_msg($msg);
|
||||
if (is_array($msg)) {
|
||||
if ($msg['status']=='pending') {
|
||||
global $smsgw;
|
||||
/* Exemple :
|
||||
{
|
||||
"number": "0612345678",
|
||||
"text": "Hello world",
|
||||
"nbfrag": 1,
|
||||
"id": "1510712464683"
|
||||
}
|
||||
*/
|
||||
$return=$smsgw->send_sms($msg['number'], $msg['text']);
|
||||
if (is_array($return)) {
|
||||
global $fpdo;
|
||||
$result = $fpdo -> update('outgoing_msg')
|
||||
-> set(
|
||||
array (
|
||||
'status' => 'pushed',
|
||||
'uniqueid' => $return['id'],
|
||||
'nbfrag' => $return['nbfrag'],
|
||||
'lastupdate' => db_now(),
|
||||
)
|
||||
)
|
||||
-> where(
|
||||
array (
|
||||
'uuid' => $msg['uuid'],
|
||||
)
|
||||
)
|
||||
-> execute();
|
||||
if ($result === false)
|
||||
return -1;
|
||||
return True;
|
||||
}
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
return True;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function get_outgoing_msg_frag($msguid, $fragid) {
|
||||
global $fpdo;
|
||||
$where=array(
|
||||
'msguid' => $msguid,
|
||||
'fragid' => $fragid,
|
||||
);
|
||||
$result = $fpdo -> from('outgoing_msg_frag')
|
||||
-> where($where)
|
||||
-> execute();
|
||||
|
||||
if ($result !== false) {
|
||||
return $result -> fetch();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function get_outgoing_msg_status_from_frags($msg) {
|
||||
if (!is_array($msg))
|
||||
$msg=get_outgoing_msg($msg);
|
||||
|
||||
if (!is_array($msg))
|
||||
return -1;
|
||||
|
||||
global $fpdo;
|
||||
$where=array(
|
||||
'msguid' => $msg['uniqueid'],
|
||||
);
|
||||
$result = $fpdo -> from('outgoing_msg_frag')
|
||||
-> where($where)
|
||||
-> execute();
|
||||
|
||||
if ($result !== false) {
|
||||
$frags=$result -> fetchAll();
|
||||
if (!is_array($frags))
|
||||
return -1;
|
||||
logging('DEBUG', "Frags : ".print_r($frags,1));
|
||||
$frag_states=array('pending','pushed','sent','delivered');
|
||||
$state_idx=0;
|
||||
$state_frag_count=0;
|
||||
foreach($frags as $frag) {
|
||||
$idx=array_search($frag['status'], $frag_states);
|
||||
if ($idx !== false) {
|
||||
if ($idx > $state_idx) {
|
||||
$state_idx=$idx;
|
||||
$state_frag_count=1;
|
||||
}
|
||||
elseif ($state_idx==$idx)
|
||||
$state_frag_count++;
|
||||
}
|
||||
}
|
||||
$status=$frag_states[$state_idx];
|
||||
if ($status=="pending" || $status=="pushed" || $state_frag_count==$msg['nbfrag']) {
|
||||
return $status;
|
||||
}
|
||||
else {
|
||||
return "partially_$status";
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function update_outgoing_msg_status($uuid, $status) {
|
||||
global $fpdo;
|
||||
$result = $fpdo -> update('outgoing_msg')
|
||||
-> set(
|
||||
array (
|
||||
'status' => $status,
|
||||
'lastupdate' => db_now(),
|
||||
)
|
||||
)
|
||||
-> where(
|
||||
array (
|
||||
'uuid' => $uuid,
|
||||
)
|
||||
)
|
||||
-> execute();
|
||||
if ($result === false)
|
||||
return -1;
|
||||
return True;
|
||||
}
|
||||
|
||||
function create_outgoing_msg_frag($msguid, $fragid, $status=false) {
|
||||
global $fpdo;
|
||||
|
||||
$values = array (
|
||||
'msguid' => $msguid,
|
||||
'fragid' => $fragid,
|
||||
);
|
||||
if ($status)
|
||||
$values['status']=strtolower($status);
|
||||
|
||||
$result = $fpdo -> insertInto('outgoing_msg_frag',$values)
|
||||
-> execute();
|
||||
if ($result === false)
|
||||
return -1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function update_outgoing_msg_frag($msguid, $fragid, $status) {
|
||||
global $fpdo;
|
||||
|
||||
$frag=get_outgoing_msg_frag($msguid, $fragid);
|
||||
|
||||
if ($frag==-1)
|
||||
return -1;
|
||||
|
||||
if (is_array($frag)) {
|
||||
$result = $fpdo -> update('outgoing_msg_frag')
|
||||
-> set(
|
||||
array (
|
||||
'status' => strtolower($status),
|
||||
)
|
||||
)
|
||||
-> where(
|
||||
array (
|
||||
'msguid' => $msguid,
|
||||
'fragid' => $fragid,
|
||||
)
|
||||
)
|
||||
-> execute();
|
||||
if ($result === false)
|
||||
return -1;
|
||||
return True;
|
||||
}
|
||||
else {
|
||||
return create_outgoing_msg_frag($msguid, $fragid, $status);
|
||||
}
|
||||
}
|
||||
|
||||
function get_smsq() {
|
||||
global $fpdo;
|
||||
$where=array(
|
||||
'status != ?' => 'delivered',
|
||||
);
|
||||
$result = $fpdo -> from('outgoing_msg')
|
||||
-> where($where)
|
||||
-> execute();
|
||||
|
||||
if ($result !== false) {
|
||||
return $result -> fetchAll();
|
||||
}
|
||||
return -1;
|
||||
}
|
158
includes/functions.php
Normal file
158
includes/functions.php
Normal file
|
@ -0,0 +1,158 @@
|
|||
<?php
|
||||
|
||||
function generate_uuid() {
|
||||
if (function_exists('random_bytes')) {
|
||||
$data=random_bytes(16);
|
||||
}
|
||||
else {
|
||||
$data=openssl_random_pseudo_bytes(16);
|
||||
}
|
||||
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
|
||||
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||
}
|
||||
|
||||
function check_phone_number($number) {
|
||||
if (preg_match('/^\+?[0-9]{10,}$/iu',$number))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function check_sms_text($text) {
|
||||
if (is_string($text) && !empty($text))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function check_integer($int) {
|
||||
if (preg_match('/^[0-9]+$/iu',$int))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
$frag_states=array('Delivered','Sent');
|
||||
function check_frag_state($state) {
|
||||
global $frag_states;
|
||||
if (in_array($state, $frag_states))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function check_uuid($uuid) {
|
||||
if (preg_match('/^[0-9A-F]{8}\-[0-9A-F]{4}\-[0-9A-F]{4}\-[0-9A-F]{4}\-[0-9A-F]{12}$/i',$uuid))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* From LdapSaisie
|
||||
*/
|
||||
|
||||
/*
|
||||
* Check email
|
||||
*/
|
||||
function check_email($value,$domain=NULL,$checkDns=true) {
|
||||
$regex = '/^((\"[^\"\f\n\r\t\v\b]+\")|([\w\!\#\$\%\&\'\*\+\-\~\/\^\`\|\{\}]+(\.[\w\!\#\$\%\&\'\*\+\-\~\/\^\`\|\{\}]+)*))@((\[(((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])))\])|(((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])))|((([A-Za-z0-9\-])+\.)+[A-Za-z\-]+))$/';
|
||||
|
||||
if (!preg_match($regex, $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$nd = explode('@', $value);
|
||||
$nd=$nd[1];
|
||||
|
||||
if ($domain) {
|
||||
if(is_array($domain)) {
|
||||
if (!in_array($nd,$domain)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if($nd!=$domain) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($checkDns && function_exists('checkdnsrr')) {
|
||||
if (!(checkdnsrr($nd, 'MX') || checkdnsrr($nd, 'A'))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime les accents d'une chaine
|
||||
*
|
||||
* @param[in] $string La chaine originale
|
||||
*
|
||||
* @retval string La chaine sans les accents
|
||||
*/
|
||||
function withoutAccents($string){
|
||||
$replaceAccent = Array(
|
||||
"à" => "a",
|
||||
"á" => "a",
|
||||
"â" => "a",
|
||||
"ã" => "a",
|
||||
"ä" => "a",
|
||||
"ç" => "c",
|
||||
"è" => "e",
|
||||
"é" => "e",
|
||||
"ê" => "e",
|
||||
"ë" => "e",
|
||||
"ì" => "i",
|
||||
"í" => "i",
|
||||
"î" => "i",
|
||||
"ï" => "i",
|
||||
"ñ" => "n",
|
||||
"ò" => "o",
|
||||
"ó" => "o",
|
||||
"ô" => "o",
|
||||
"õ" => "o",
|
||||
"ö" => "o",
|
||||
"ù" => "u",
|
||||
"ú" => "u",
|
||||
"û" => "u",
|
||||
"ü" => "u",
|
||||
"ý" => "y",
|
||||
"ÿ" => "y",
|
||||
"À" => "A",
|
||||
"Á" => "A",
|
||||
"Â" => "A",
|
||||
"Ã" => "A",
|
||||
"Ä" => "A",
|
||||
"Ç" => "C",
|
||||
"È" => "E",
|
||||
"É" => "E",
|
||||
"Ê" => "E",
|
||||
"Ë" => "E",
|
||||
"Ì" => "I",
|
||||
"Í" => "I",
|
||||
"Î" => "I",
|
||||
"Ï" => "I",
|
||||
"Ñ" => "N",
|
||||
"Ò" => "O",
|
||||
"Ó" => "O",
|
||||
"Ô" => "O",
|
||||
"Õ" => "O",
|
||||
"Ö" => "O",
|
||||
"Ù" => "U",
|
||||
"Ú" => "U",
|
||||
"Û" => "U",
|
||||
"Ü" => "U",
|
||||
"Ý" => "Y"
|
||||
);
|
||||
return strtr($string, $replaceAccent);
|
||||
}
|
||||
|
||||
function varDump($data) {
|
||||
ob_start();
|
||||
var_dump($data);
|
||||
$data=ob_get_contents();
|
||||
ob_end_clean();
|
||||
return $data;
|
||||
}
|
43
includes/logging.php
Normal file
43
includes/logging.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Configuration :
|
||||
* // Log file
|
||||
* $log_file='/path/to/app.log';
|
||||
*
|
||||
* // Log level (DEBUG / INFO / WARNING / ERROR)
|
||||
* $log_level='INFO';
|
||||
*/
|
||||
|
||||
|
||||
// Log file descriptor (Do not change !!!)
|
||||
$_log_file_fd=null;
|
||||
|
||||
// Log Levels
|
||||
$_log_levels=array(
|
||||
'DEBUG' => 0,
|
||||
'INFO' => 1,
|
||||
'WARNING' => 2,
|
||||
'ERROR' => 3,
|
||||
);
|
||||
|
||||
function logging($level,$message) {
|
||||
global $log_file, $_log_file_fd, $_log_levels, $log_level;
|
||||
|
||||
if (!array_key_exists($level, $_log_levels)) $level=='INFO';
|
||||
$level_id=$_log_levels[$level];
|
||||
|
||||
if (!array_key_exists($log_level, $_log_levels)) $log_level=='INFO';
|
||||
$log_level_id=$_log_levels[$log_level];
|
||||
|
||||
if ($level_id<$log_level_id) return true;
|
||||
if(is_null($_log_file_fd)) {
|
||||
$_log_file_fd=fopen($log_file,'a');
|
||||
}
|
||||
|
||||
$msg=date('Y/m/d H:i:s').' - '.$_SERVER['REQUEST_URI'].' - '.$_SERVER['REMOTE_ADDR']." - $level - $message\n";
|
||||
|
||||
fwrite($_log_file_fd,$msg);
|
||||
|
||||
return true;
|
||||
}
|
111
includes/sms_gw_api.php
Normal file
111
includes/sms_gw_api.php
Normal file
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
Class sms_gw_api {
|
||||
|
||||
// WebServices parameters
|
||||
private $ws_url = null;
|
||||
private $ws_ssl_verify = true;
|
||||
|
||||
function sms_gw_api($ws_url, $ws_ssl_verify=true){
|
||||
$this -> ws_url = $ws_url;
|
||||
$this -> ws_ssl_verify = $ws_ssl_verify;
|
||||
}
|
||||
|
||||
/*
|
||||
* REST Request
|
||||
*/
|
||||
|
||||
private function _ws_call($method, $url, $data = false, $json = true) {
|
||||
if (is_null($this -> ws_url)) return;
|
||||
|
||||
$curl = curl_init();
|
||||
|
||||
switch ($method) {
|
||||
case "POST":
|
||||
curl_setopt($curl, CURLOPT_POST, 1);
|
||||
|
||||
if ($data)
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
|
||||
break;
|
||||
case "PUT":
|
||||
curl_setopt($curl, CURLOPT_PUT, 1);
|
||||
break;
|
||||
default:
|
||||
$method='GET';
|
||||
if ($data)
|
||||
$url = sprintf("%s?%s", $url, http_build_query($data));
|
||||
}
|
||||
|
||||
if ($this -> ws_ssl_verify) {
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, TRUE);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
}
|
||||
else {
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 1);
|
||||
}
|
||||
|
||||
$complete_url=$this -> ws_url.'/'.$url;
|
||||
curl_setopt($curl, CURLOPT_URL, $complete_url);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
logging("DEBUG", "sms_gw_api : Call URL $complete_url");
|
||||
|
||||
$result = curl_exec($curl);
|
||||
|
||||
if(curl_errno($curl)) {
|
||||
logging("ERROR", "sms_gw_api : Error during $method request to ".$complete_url." : ".curl_error($curl));
|
||||
curl_close($curl);
|
||||
return False;
|
||||
}
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
logging("DEBUG","sms_gw_api : Return => '$result'");
|
||||
|
||||
if (!$json)
|
||||
return $result;
|
||||
|
||||
try {
|
||||
$data = json_decode($result,true);
|
||||
logging("DEBUG","sms_gw_api : Decoded data :".print_r($data,1));
|
||||
return $data;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
logging("ERROR","sms_gw_api : Error parsing $method request JSON result on ".$complete_url." : ".$e);
|
||||
return False;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* send sms
|
||||
* @param string number
|
||||
* @param string text
|
||||
* @return struct sms gw return struct
|
||||
*/
|
||||
function send_sms($number, $text){
|
||||
return self :: _ws_call(
|
||||
'GET',
|
||||
"",
|
||||
array(
|
||||
'number' => $number,
|
||||
'text' => $text,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* check gateway is alive
|
||||
* @return boolean sms gw status
|
||||
*/
|
||||
function check_gateway(){
|
||||
$return=self :: _ws_call(
|
||||
'GET',
|
||||
"",
|
||||
array(),
|
||||
false
|
||||
);
|
||||
return ($return !== false && preg_match('/My SMS Gateway/', $return));
|
||||
}
|
||||
|
||||
}
|
9
public_html/.htaccess
Normal file
9
public_html/.htaccess
Normal file
|
@ -0,0 +1,9 @@
|
|||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# If the request is not for a valid file
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
# If the request is not for a valid link
|
||||
RewriteCond %{REQUEST_FILENAME} !-l
|
||||
RewriteRule ^([^?]+)(\?(.*))?$ index.php?go=$1&$3 [L,QSA]
|
||||
|
126
public_html/index.php
Normal file
126
public_html/index.php
Normal file
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
include '../includes/core.php';
|
||||
|
||||
$go=false;
|
||||
if (isset($_REQUEST['go'])) {
|
||||
$go=$_REQUEST['go'];
|
||||
}
|
||||
|
||||
$post_body=file_get_contents('php://input');
|
||||
if ($post_body) {
|
||||
$post_body=json_decode($post_body, true);
|
||||
if(!is_array($post_body)) $post_body=false;
|
||||
}
|
||||
else {
|
||||
$post_body=false;
|
||||
}
|
||||
logging('DEBUG','POST Body : '.print_r($post_body,1));
|
||||
|
||||
$data=array();
|
||||
switch($go) {
|
||||
case 'send_sms':
|
||||
if (check_phone_number($_REQUEST['number']) && check_sms_text($_REQUEST['text'])) {
|
||||
logging('INFO','New outgoing message for '.$_REQUEST['number']);
|
||||
$uuid = create_outgoing_msg($_REQUEST['number'], $_REQUEST['text']);
|
||||
if (check_uuid($uuid)) {
|
||||
$data['uuid']=$uuid;
|
||||
if(handle_outgoing_msg($uuid)===True) {
|
||||
$data['status']='ok';
|
||||
}
|
||||
else {
|
||||
$data['status']='pending';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$data['status']='error';
|
||||
$data['msg']='Error creating your outgoing message';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$data['status']='error';
|
||||
$data['msg']='Invalid parameters';
|
||||
}
|
||||
break;
|
||||
case 'ack':
|
||||
/*
|
||||
Post body example : Array (
|
||||
[id] => 1510702925705
|
||||
[state] => Delivered
|
||||
[frag] => 0
|
||||
[text] => qsdmlkqsdmlqskdmqlksdmqlsdkmqlsdqsdmlkqsdmlqskdmqlksdmqlsdkmqlsdqsdmlkqsdmlqskdmqlksdmqlsdkmqlsdqsdmlkqsdmlqskdmqlksdmqlsdkmqlsdqsdmlkqsdmlqskdmqlksdmqls
|
||||
)
|
||||
*/
|
||||
if (is_array($post_body) && check_integer($post_body['id']) && check_integer($post_body['frag']) && check_frag_state($post_body['state'])) {
|
||||
logging('INFO','Ack for message '.$post_body['id'].' (Frag : '.$post_body['frag'].') with status '.$post_body['state']);
|
||||
$msg=get_outgoing_msg_by_uniqueid($post_body['id']);
|
||||
if (is_array($msg)) {
|
||||
logging('INFO',"Corresponding outgoing msg : ".$msg['uuid']);
|
||||
logging('DEBUG',"Outgoing msg : ".print_r($msg,1));
|
||||
if (update_outgoing_msg_frag($post_body['id'], $post_body['frag'], strtolower($post_body['state']))===True) {
|
||||
$msg_status=get_outgoing_msg_status_from_frags($msg);
|
||||
if ($msg_status!=$mgs['status'] && update_outgoing_msg_status($msg['uuid'], $msg_status)==-1) {
|
||||
$data['status']='error';
|
||||
$data['msg']='Error updating outgoing msg status';
|
||||
}
|
||||
else {
|
||||
$data['status']='ok';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$data['status']='error';
|
||||
$data['msg']='Fail to update outgoing msg frag';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$data['status']='error';
|
||||
$data['msg']='Outgoing message not found';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$data['status']='error';
|
||||
$data['msg']='Invalid parameters';
|
||||
}
|
||||
break;
|
||||
case 'incoming':
|
||||
/*
|
||||
Post body example : Array (
|
||||
[number] => +33612345678
|
||||
[text] => qsdmlkqsdmlqskdmqlksd
|
||||
[timestampMillis] => 1510702926820
|
||||
)
|
||||
*/
|
||||
if (is_array($post_body) && check_phone_number($post_body['number']) && check_sms_text($post_body['text']) && check_integer($post_body['timestampMillis'])) {
|
||||
logging('INFO','Incoming SMS from '.$post_body['number']);
|
||||
$msg=create_incoming_msg($post_body['number'], $post_body['text'], $post_body['timestampMillis']);
|
||||
if (is_array($msg)) {
|
||||
$data['status']='ok';
|
||||
}
|
||||
else {
|
||||
$data['status']='error';
|
||||
$data['msg']='Error creating your incoming message';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$data['status']='error';
|
||||
$data['msg']='Invalid parameters';
|
||||
}
|
||||
|
||||
break;
|
||||
case 'smsq':
|
||||
$data['messages']=get_smsq();
|
||||
logging('DEBUG','SMSQ : '.print_r($data['messages'],1));
|
||||
break;
|
||||
case 'check_gateway':
|
||||
$data['status']=($smsgw->check_gateway()?'alive':'not responding');
|
||||
logging('DEBUG','SMS Gateway Status : '.$data['status']);
|
||||
break;
|
||||
case '':
|
||||
print "Welcome on SMS Gateway WebService";
|
||||
exit();
|
||||
break;
|
||||
default:
|
||||
$data['status']='error';
|
||||
$data['msg']='Invalid request';
|
||||
}
|
||||
print json_encode($data, (isset($_REQUEST['pretty'])?JSON_PRETTY_PRINT:0));
|
8
tests/db-get-outgoing-msg-by-uniqueid.php
Normal file
8
tests/db-get-outgoing-msg-by-uniqueid.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
require '../includes/core.php';
|
||||
|
||||
if (count($argv) != 2 || !check_integer($argv[1])) {
|
||||
die("Usage : $argv[0] [uuid]\n");
|
||||
}
|
||||
var_dump(get_outgoing_msg_by_uniqueid($argv[1]));
|
8
tests/db-get-outgoing-msg-status-from-frags.php
Normal file
8
tests/db-get-outgoing-msg-status-from-frags.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
require '../includes/core.php';
|
||||
|
||||
if (count($argv) != 2 || !check_uuid($argv[1])) {
|
||||
die("Usage : $argv[0] [uuid]\n");
|
||||
}
|
||||
var_dump(get_outgoing_msg_status_from_frags($argv[1]));
|
8
tests/db-get-outgoing-msg.php
Normal file
8
tests/db-get-outgoing-msg.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
require '../includes/core.php';
|
||||
|
||||
if (count($argv) != 2 || !check_uuid($argv[1])) {
|
||||
die("Usage : $argv[0] [uuid]\n");
|
||||
}
|
||||
var_dump(get_outgoing_msg($argv[1]));
|
8
tests/db-handle-outgoing-msg.php
Normal file
8
tests/db-handle-outgoing-msg.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
require '../includes/core.php';
|
||||
|
||||
if (count($argv) != 2 || !check_uuid($argv[1])) {
|
||||
die("Usage : $argv[0] [uuid]\n");
|
||||
}
|
||||
var_dump(handle_outgoing_msg($argv[1]));
|
9
tests/db-update-outgoing-msg-frag.php
Normal file
9
tests/db-update-outgoing-msg-frag.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
require '../includes/core.php';
|
||||
|
||||
if (count($argv) != 4 || !check_integer($argv[1]) || !check_integer($argv[2]) || !check_frag_state($argv[3])) {
|
||||
die("Usage : $argv[0] [uniqueid] [frag id] [status]\n");
|
||||
}
|
||||
// function update_outgoing_msg_frag($msguid, $fragid, $status)
|
||||
var_dump(update_outgoing_msg_frag($argv[1], $argv[2], $argv[3]));
|
361
vendor/FluentPDO/BaseQuery.php
vendored
Normal file
361
vendor/FluentPDO/BaseQuery.php
vendored
Normal file
|
@ -0,0 +1,361 @@
|
|||
<?php
|
||||
|
||||
/** Base query builder
|
||||
*/
|
||||
abstract class BaseQuery implements IteratorAggregate
|
||||
{
|
||||
|
||||
/** @var FluentPDO */
|
||||
private $fpdo;
|
||||
|
||||
/** @var PDOStatement */
|
||||
private $result;
|
||||
|
||||
/** @var float */
|
||||
private $time;
|
||||
|
||||
/** @var bool */
|
||||
private $object = false;
|
||||
|
||||
/** @var array - definition clauses */
|
||||
protected $clauses = array();
|
||||
/** @var array */
|
||||
protected $statements = array();
|
||||
/** @var array */
|
||||
protected $parameters = array();
|
||||
|
||||
/**
|
||||
* BaseQuery constructor.
|
||||
*
|
||||
* @param FluentPDO $fpdo
|
||||
* @param $clauses
|
||||
*/
|
||||
protected function __construct(FluentPDO $fpdo, $clauses) {
|
||||
$this->fpdo = $fpdo;
|
||||
$this->clauses = $clauses;
|
||||
$this->initClauses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize statement and parameter clauses.
|
||||
*/
|
||||
private function initClauses() {
|
||||
foreach ($this->clauses as $clause => $value) {
|
||||
if ($value) {
|
||||
$this->statements[$clause] = array();
|
||||
$this->parameters[$clause] = array();
|
||||
} else {
|
||||
$this->statements[$clause] = null;
|
||||
$this->parameters[$clause] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add statement for all kind of clauses
|
||||
*
|
||||
* @param $clause
|
||||
* @param $statement
|
||||
* @param array $parameters
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function addStatement($clause, $statement, $parameters = array()) {
|
||||
if ($statement === null) {
|
||||
return $this->resetClause($clause);
|
||||
}
|
||||
// $statement !== null
|
||||
if ($this->clauses[$clause]) {
|
||||
if (is_array($statement)) {
|
||||
$this->statements[$clause] = array_merge($this->statements[$clause], $statement);
|
||||
} else {
|
||||
$this->statements[$clause][] = $statement;
|
||||
}
|
||||
$this->parameters[$clause] = array_merge($this->parameters[$clause], $parameters);
|
||||
} else {
|
||||
$this->statements[$clause] = $statement;
|
||||
$this->parameters[$clause] = $parameters;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all prev defined statements
|
||||
*
|
||||
* @param $clause
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function resetClause($clause) {
|
||||
$this->statements[$clause] = null;
|
||||
$this->parameters[$clause] = array();
|
||||
if (isset($this->clauses[$clause]) && $this->clauses[$clause]) {
|
||||
$this->statements[$clause] = array();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements method from IteratorAggregate
|
||||
*
|
||||
* @return \PDOStatement
|
||||
*/
|
||||
public function getIterator() {
|
||||
return $this->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute query with earlier added parameters
|
||||
*
|
||||
* @return \PDOStatement
|
||||
*/
|
||||
public function execute() {
|
||||
$query = $this->buildQuery();
|
||||
$parameters = $this->buildParameters();
|
||||
|
||||
$result = $this->fpdo->getPdo()->prepare($query);
|
||||
|
||||
// At this point, $result is a PDOStatement instance, or false.
|
||||
// PDO::prepare() does not reliably return errors. Some database drivers
|
||||
// do not support prepared statements, and PHP emulates them. Postgresql
|
||||
// does support prepared statements, but PHP does not call Postgresql's
|
||||
// prepare function until we call PDOStatement::execute() below.
|
||||
// If PDO::prepare() worked properly, this is where we would check
|
||||
// for prepare errors, such as invalid SQL.
|
||||
|
||||
if ($this->object !== false) {
|
||||
if (class_exists($this->object)) {
|
||||
$result->setFetchMode(PDO::FETCH_CLASS, $this->object);
|
||||
} else {
|
||||
$result->setFetchMode(PDO::FETCH_OBJ);
|
||||
}
|
||||
} elseif ($this->fpdo->getPdo()->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE) == PDO::FETCH_BOTH) {
|
||||
$result->setFetchMode(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$time = microtime(true);
|
||||
if ($result && $result->execute($parameters)) {
|
||||
$this->time = microtime(true) - $time;
|
||||
} else {
|
||||
$result = false;
|
||||
}
|
||||
|
||||
$this->result = $result;
|
||||
$this->debugger();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Echo/pass a debug string
|
||||
*/
|
||||
private function debugger() {
|
||||
if ($this->fpdo->debug) {
|
||||
if (!is_callable($this->fpdo->debug)) {
|
||||
$backtrace = '';
|
||||
$query = $this->getQuery();
|
||||
$parameters = $this->getParameters();
|
||||
$debug = '';
|
||||
if ($parameters) {
|
||||
$debug = '# parameters: ' . implode(', ', array_map(array($this, 'quote'), $parameters)) . "\n";
|
||||
}
|
||||
$debug .= $query;
|
||||
$pattern = '(^' . preg_quote(__DIR__) . '(\\.php$|[/\\\\]))'; // can be static
|
||||
foreach (debug_backtrace() as $backtrace) {
|
||||
if (isset($backtrace['file']) && !preg_match($pattern, $backtrace['file'])) {
|
||||
// stop on first file outside FluentPDO source codes
|
||||
break;
|
||||
}
|
||||
}
|
||||
$time = sprintf('%0.3f', $this->time * 1000) . ' ms';
|
||||
$rows = ($this->result) ? $this->result->rowCount() : 0;
|
||||
$finalString = "# $backtrace[file]:$backtrace[line] ($time; rows = $rows)\n$debug\n\n";
|
||||
if (is_resource(STDERR)) { // if STDERR is set, send there, otherwise just output the string
|
||||
fwrite(STDERR, $finalString);
|
||||
}
|
||||
else {
|
||||
echo $finalString;
|
||||
}
|
||||
} else {
|
||||
call_user_func($this->fpdo->debug, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \PDO
|
||||
*/
|
||||
protected function getPDO() {
|
||||
return $this->fpdo->getPdo();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \FluentStructure
|
||||
*/
|
||||
protected function getStructure() {
|
||||
return $this->fpdo->getStructure();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDOStatement result
|
||||
*
|
||||
* @return \PDOStatement
|
||||
*/
|
||||
public function getResult() {
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time of execution
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getTime() {
|
||||
return $this->time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query parameters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getParameters() {
|
||||
return $this->buildParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query string
|
||||
*
|
||||
* @param bool $formatted - Return formatted query
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQuery($formatted = true) {
|
||||
$query = $this->buildQuery();
|
||||
if ($formatted) {
|
||||
$query = FluentUtils::formatQuery($query);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate query
|
||||
*
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function buildQuery() {
|
||||
$query = '';
|
||||
foreach ($this->clauses as $clause => $separator) {
|
||||
if ($this->clauseNotEmpty($clause)) {
|
||||
if (is_string($separator)) {
|
||||
$query .= " $clause " . implode($separator, $this->statements[$clause]);
|
||||
} elseif ($separator === null) {
|
||||
$query .= " $clause " . $this->statements[$clause];
|
||||
} elseif (is_callable($separator)) {
|
||||
$query .= call_user_func($separator);
|
||||
} else {
|
||||
throw new Exception("Clause '$clause' is incorrectly set to '$separator'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return trim($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $clause
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function clauseNotEmpty($clause) {
|
||||
if ($this->clauses[$clause]) {
|
||||
return (boolean)count($this->statements[$clause]);
|
||||
} else {
|
||||
return (boolean)$this->statements[$clause];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function buildParameters() {
|
||||
$parameters = array();
|
||||
foreach ($this->parameters as $clauses) {
|
||||
if (is_array($clauses)) {
|
||||
foreach ($clauses as $value) {
|
||||
if (is_array($value) && is_string(key($value)) && substr(key($value), 0, 1) == ':') {
|
||||
// this is named params e.g. (':name' => 'Mark')
|
||||
$parameters = array_merge($parameters, $value);
|
||||
} else {
|
||||
$parameters[] = $value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($clauses) {
|
||||
$parameters[] = $clauses;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function quote($value) {
|
||||
if (!isset($value)) {
|
||||
return "NULL";
|
||||
}
|
||||
if (is_array($value)) { // (a, b) IN ((1, 2), (3, 4))
|
||||
return "(" . implode(", ", array_map(array($this, 'quote'), $value)) . ")";
|
||||
}
|
||||
$value = $this->formatValue($value);
|
||||
if (is_float($value)) {
|
||||
return sprintf("%F", $value); // otherwise depends on setlocale()
|
||||
}
|
||||
if ($value === false) {
|
||||
return "0";
|
||||
}
|
||||
if (is_int($value) || $value instanceof FluentLiteral) { // number or SQL code - for example "NOW()"
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
return $this->fpdo->getPdo()->quote($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $val
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function formatValue($val) {
|
||||
if ($val instanceof DateTime) {
|
||||
return $val->format("Y-m-d H:i:s"); //! may be driver specific
|
||||
}
|
||||
|
||||
return $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an item as object
|
||||
*
|
||||
* @param boolean|object $object If set to true, items are returned as stdClass, otherwise a class
|
||||
* name can be passed and a new instance of this class is return.
|
||||
* Can be set to false to return items as an associative array.
|
||||
*
|
||||
* @return \BaseQuery
|
||||
*/
|
||||
public function asObject($object = true) {
|
||||
$this->object = $object;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
290
vendor/FluentPDO/CommonQuery.php
vendored
Normal file
290
vendor/FluentPDO/CommonQuery.php
vendored
Normal file
|
@ -0,0 +1,290 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* CommonQuery add JOIN and WHERE clauses for (SELECT, UPDATE, DELETE)
|
||||
*/
|
||||
abstract class CommonQuery extends BaseQuery
|
||||
{
|
||||
|
||||
/** @var array - Query tables (also include table from clause FROM) */
|
||||
protected $joins = array();
|
||||
|
||||
/** @var bool - Disable adding undefined joins to query? */
|
||||
protected $isSmartJoinEnabled = true;
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function enableSmartJoin() {
|
||||
$this->isSmartJoinEnabled = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function disableSmartJoin() {
|
||||
$this->isSmartJoinEnabled = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSmartJoinEnabled() {
|
||||
return $this->isSmartJoinEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add where condition, more calls appends with AND
|
||||
*
|
||||
* @param string $condition possibly containing ? or :name (PDO syntax)
|
||||
* @param mixed $parameters array or a scalar value
|
||||
*
|
||||
* @return \SelectQuery
|
||||
*/
|
||||
public function where($condition, $parameters = array()) {
|
||||
if ($condition === null) {
|
||||
return $this->resetClause('WHERE');
|
||||
}
|
||||
if (!$condition) {
|
||||
return $this;
|
||||
}
|
||||
if (is_array($condition)) { // where(array("column1" => 1, "column2 > ?" => 2))
|
||||
foreach ($condition as $key => $val) {
|
||||
$this->where($key, $val);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
$args = func_get_args();
|
||||
if (count($args) == 1) {
|
||||
return $this->addStatement('WHERE', $condition);
|
||||
}
|
||||
|
||||
// check that there are 2 arguments, a condition and a parameter value
|
||||
// if the condition contains a parameter simply add them
|
||||
// since its up to the user if it's valid sql or not
|
||||
// Otherwise we're probably with just an identifier. So lets
|
||||
// construct a new condition based on the passed parameter value.
|
||||
if (count($args) == 2 && !preg_match('/(\?|:\w+)/i', $condition)) {
|
||||
// condition is column only
|
||||
if (is_null($parameters)) {
|
||||
return $this->addStatement('WHERE', "$condition is NULL");
|
||||
} elseif ($args[1] === array()) {
|
||||
return $this->addStatement('WHERE', 'FALSE');
|
||||
} elseif (is_array($args[1])) {
|
||||
$in = $this->quote($args[1]);
|
||||
|
||||
return $this->addStatement('WHERE', "$condition IN $in");
|
||||
}
|
||||
$condition = "$condition = ?";
|
||||
}
|
||||
array_shift($args);
|
||||
|
||||
return $this->addStatement('WHERE', $condition, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $clause
|
||||
* @param array $parameters - first is $statement followed by $parameters
|
||||
*
|
||||
* @return $this|\SelectQuery
|
||||
*/
|
||||
public function __call($clause, $parameters = array()) {
|
||||
$clause = FluentUtils::toUpperWords($clause);
|
||||
if ($clause == 'GROUP') {
|
||||
$clause = 'GROUP BY';
|
||||
}
|
||||
if ($clause == 'ORDER') {
|
||||
$clause = 'ORDER BY';
|
||||
}
|
||||
if ($clause == 'FOOT NOTE') {
|
||||
$clause = "\n--";
|
||||
}
|
||||
$statement = array_shift($parameters);
|
||||
if (strpos($clause, 'JOIN') !== false) {
|
||||
return $this->addJoinStatements($clause, $statement, $parameters);
|
||||
}
|
||||
|
||||
return $this->addStatement($clause, $statement, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getClauseJoin() {
|
||||
return implode(' ', $this->statements['JOIN']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Statement can contain more tables (e.g. "table1.table2:table3:")
|
||||
*
|
||||
* @param $clause
|
||||
* @param $statement
|
||||
* @param array $parameters
|
||||
*
|
||||
* @return $this|\SelectQuery
|
||||
*/
|
||||
private function addJoinStatements($clause, $statement, $parameters = array()) {
|
||||
if ($statement === null) {
|
||||
$this->joins = array();
|
||||
|
||||
return $this->resetClause('JOIN');
|
||||
}
|
||||
if (array_search(substr($statement, 0, -1), $this->joins) !== false) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// match "tables AS alias"
|
||||
preg_match('/`?([a-z_][a-z0-9_\.:]*)`?(\s+AS)?(\s+`?([a-z_][a-z0-9_]*)`?)?/i', $statement, $matches);
|
||||
$joinAlias = '';
|
||||
$joinTable = '';
|
||||
if ($matches) {
|
||||
$joinTable = $matches[1];
|
||||
if (isset($matches[4]) && !in_array(strtoupper($matches[4]), array('ON', 'USING'))) {
|
||||
$joinAlias = $matches[4];
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos(strtoupper($statement), ' ON ') || strpos(strtoupper($statement), ' USING')) {
|
||||
if (!$joinAlias) {
|
||||
$joinAlias = $joinTable;
|
||||
}
|
||||
if (in_array($joinAlias, $this->joins)) {
|
||||
return $this;
|
||||
} else {
|
||||
$this->joins[] = $joinAlias;
|
||||
$statement = " $clause $statement";
|
||||
|
||||
return $this->addStatement('JOIN', $statement, $parameters);
|
||||
}
|
||||
}
|
||||
|
||||
// $joinTable is list of tables for join e.g.: table1.table2:table3....
|
||||
if (!in_array(substr($joinTable, -1), array('.', ':'))) {
|
||||
$joinTable .= '.';
|
||||
}
|
||||
|
||||
preg_match_all('/([a-z_][a-z0-9_]*[\.:]?)/i', $joinTable, $matches);
|
||||
$mainTable = '';
|
||||
if (isset($this->statements['FROM'])) {
|
||||
$mainTable = $this->statements['FROM'];
|
||||
} elseif (isset($this->statements['UPDATE'])) {
|
||||
$mainTable = $this->statements['UPDATE'];
|
||||
}
|
||||
$lastItem = array_pop($matches[1]);
|
||||
array_push($matches[1], $lastItem);
|
||||
foreach ($matches[1] as $joinItem) {
|
||||
if ($mainTable == substr($joinItem, 0, -1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// use $joinAlias only for $lastItem
|
||||
$alias = '';
|
||||
if ($joinItem == $lastItem) {
|
||||
$alias = $joinAlias;
|
||||
}
|
||||
|
||||
$newJoin = $this->createJoinStatement($clause, $mainTable, $joinItem, $alias);
|
||||
if ($newJoin) {
|
||||
$this->addStatement('JOIN', $newJoin, $parameters);
|
||||
}
|
||||
$mainTable = $joinItem;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create join string
|
||||
*
|
||||
* @param $clause
|
||||
* @param $mainTable
|
||||
* @param $joinTable
|
||||
* @param string $joinAlias
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function createJoinStatement($clause, $mainTable, $joinTable, $joinAlias = '') {
|
||||
if (in_array(substr($mainTable, -1), array(':', '.'))) {
|
||||
$mainTable = substr($mainTable, 0, -1);
|
||||
}
|
||||
$referenceDirection = substr($joinTable, -1);
|
||||
$joinTable = substr($joinTable, 0, -1);
|
||||
$asJoinAlias = '';
|
||||
if ($joinAlias) {
|
||||
$asJoinAlias = " AS $joinAlias";
|
||||
} else {
|
||||
$joinAlias = $joinTable;
|
||||
}
|
||||
if (in_array($joinAlias, $this->joins)) {
|
||||
// if join exists don't create same again
|
||||
return '';
|
||||
} else {
|
||||
$this->joins[] = $joinAlias;
|
||||
}
|
||||
if ($referenceDirection == ':') {
|
||||
// back reference
|
||||
$primaryKey = $this->getStructure()->getPrimaryKey($mainTable);
|
||||
$foreignKey = $this->getStructure()->getForeignKey($mainTable);
|
||||
|
||||
return " $clause $joinTable$asJoinAlias ON $joinAlias.$foreignKey = $mainTable.$primaryKey";
|
||||
} else {
|
||||
$primaryKey = $this->getStructure()->getPrimaryKey($joinTable);
|
||||
$foreignKey = $this->getStructure()->getForeignKey($joinTable);
|
||||
|
||||
return " $clause $joinTable$asJoinAlias ON $joinAlias.$primaryKey = $mainTable.$foreignKey";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function buildQuery() {
|
||||
// first create extra join from statements with columns with referenced tables
|
||||
$statementsWithReferences = array('WHERE', 'SELECT', 'GROUP BY', 'ORDER BY');
|
||||
foreach ($statementsWithReferences as $clause) {
|
||||
if (array_key_exists($clause, $this->statements)) {
|
||||
$this->statements[$clause] = array_map(array($this, 'createUndefinedJoins'), $this->statements[$clause]);
|
||||
}
|
||||
}
|
||||
|
||||
return parent::buildQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create undefined joins from statement with column with referenced tables
|
||||
*
|
||||
* @param string $statement
|
||||
*
|
||||
* @return string rewrited $statement (e.g. tab1.tab2:col => tab2.col)
|
||||
*/
|
||||
private function createUndefinedJoins($statement) {
|
||||
if (!$this->isSmartJoinEnabled) {
|
||||
return $statement;
|
||||
}
|
||||
|
||||
preg_match_all('/\\b([a-z_][a-z0-9_.:]*[.:])[a-z_]*/i', $statement, $matches);
|
||||
foreach ($matches[1] as $join) {
|
||||
if (!in_array(substr($join, 0, -1), $this->joins)) {
|
||||
$this->addJoinStatements('LEFT JOIN', $join);
|
||||
}
|
||||
}
|
||||
|
||||
// don't rewrite table from other databases
|
||||
foreach ($this->joins as $join) {
|
||||
if (strpos($join, '.') !== false && strpos($statement, $join) === 0) {
|
||||
return $statement;
|
||||
}
|
||||
}
|
||||
|
||||
// remove extra referenced tables (rewrite tab1.tab2:col => tab2.col)
|
||||
$statement = preg_replace('/(?:\\b[a-z_][a-z0-9_.:]*[.:])?([a-z_][a-z0-9_]*)[.:]([a-z_*])/i', '\\1.\\2', $statement);
|
||||
|
||||
return $statement;
|
||||
}
|
||||
|
||||
}
|
94
vendor/FluentPDO/DeleteQuery.php
vendored
Normal file
94
vendor/FluentPDO/DeleteQuery.php
vendored
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* DELETE query builder
|
||||
*
|
||||
* @method DeleteQuery leftJoin(string $statement) add LEFT JOIN to query
|
||||
* ($statement can be 'table' name only or 'table:' means back reference)
|
||||
* @method DeleteQuery innerJoin(string $statement) add INNER JOIN to query
|
||||
* ($statement can be 'table' name only or 'table:' means back reference)
|
||||
* @method DeleteQuery from(string $table) add LIMIT to query
|
||||
* @method DeleteQuery orderBy(string $column) add ORDER BY to query
|
||||
* @method DeleteQuery limit(int $limit) add LIMIT to query
|
||||
*/
|
||||
class DeleteQuery extends CommonQuery
|
||||
{
|
||||
|
||||
private $ignore = false;
|
||||
|
||||
/**
|
||||
* DeleteQuery constructor.
|
||||
*
|
||||
* @param FluentPDO $fpdo
|
||||
* @param string $table
|
||||
*/
|
||||
public function __construct(FluentPDO $fpdo, $table) {
|
||||
$clauses = array(
|
||||
'DELETE FROM' => array($this, 'getClauseDeleteFrom'),
|
||||
'DELETE' => array($this, 'getClauseDelete'),
|
||||
'FROM' => null,
|
||||
'JOIN' => array($this, 'getClauseJoin'),
|
||||
'WHERE' => ' AND ',
|
||||
'ORDER BY' => ', ',
|
||||
'LIMIT' => null,
|
||||
);
|
||||
|
||||
parent::__construct($fpdo, $clauses);
|
||||
|
||||
$this->statements['DELETE FROM'] = $table;
|
||||
$this->statements['DELETE'] = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces delete operation to fail silently
|
||||
*
|
||||
* @return \DeleteQuery
|
||||
*/
|
||||
public function ignore() {
|
||||
$this->ignore = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function buildQuery() {
|
||||
if ($this->statements['FROM']) {
|
||||
unset($this->clauses['DELETE FROM']);
|
||||
} else {
|
||||
unset($this->clauses['DELETE']);
|
||||
}
|
||||
|
||||
return parent::buildQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute DELETE query
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function execute() {
|
||||
$result = parent::execute();
|
||||
if ($result) {
|
||||
return $result->rowCount();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getClauseDelete() {
|
||||
return 'DELETE' . ($this->ignore ? " IGNORE" : '') . ' ' . $this->statements['DELETE'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getClauseDeleteFrom() {
|
||||
return 'DELETE' . ($this->ignore ? " IGNORE" : '') . ' FROM ' . $this->statements['DELETE FROM'];
|
||||
}
|
||||
|
||||
}
|
30
vendor/FluentPDO/FluentLiteral.php
vendored
Normal file
30
vendor/FluentPDO/FluentLiteral.php
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* SQL literal value
|
||||
*/
|
||||
class FluentLiteral
|
||||
{
|
||||
|
||||
/** @var string */
|
||||
protected $value = '';
|
||||
|
||||
/**
|
||||
* Create literal value
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
function __construct($value) {
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get literal value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function __toString() {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
}
|
164
vendor/FluentPDO/FluentPDO.php
vendored
Normal file
164
vendor/FluentPDO/FluentPDO.php
vendored
Normal file
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
/**
|
||||
* FluentPDO is simple and smart SQL query builder for PDO
|
||||
*
|
||||
* For more information @see readme.md
|
||||
*
|
||||
* @link http://github.com/lichtner/fluentpdo
|
||||
* @author Marek Lichtner, marek@licht.sk
|
||||
* @copyright 2012 Marek Lichtner
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
|
||||
* @license http://www.gnu.org/licenses/gpl-2.0.html GNU General Public License, version 2 (one or other)
|
||||
*/
|
||||
|
||||
include_once 'FluentStructure.php';
|
||||
include_once 'FluentUtils.php';
|
||||
include_once 'FluentLiteral.php';
|
||||
include_once 'BaseQuery.php';
|
||||
include_once 'CommonQuery.php';
|
||||
include_once 'SelectQuery.php';
|
||||
include_once 'InsertQuery.php';
|
||||
include_once 'UpdateQuery.php';
|
||||
include_once 'DeleteQuery.php';
|
||||
|
||||
/**
|
||||
* Class FluentPDO
|
||||
*/
|
||||
class FluentPDO
|
||||
{
|
||||
|
||||
/** @var \PDO */
|
||||
protected $pdo;
|
||||
/** @var \FluentStructure|null */
|
||||
protected $structure;
|
||||
|
||||
/** @var bool|callback */
|
||||
public $debug;
|
||||
|
||||
/** @var boolean */
|
||||
public $convertTypes = false;
|
||||
|
||||
/**
|
||||
* FluentPDO constructor.
|
||||
*
|
||||
* @param \PDO $pdo
|
||||
* @param \FluentStructure|null $structure
|
||||
*/
|
||||
function __construct(PDO $pdo, FluentStructure $structure = null) {
|
||||
$this->pdo = $pdo;
|
||||
if (!$structure) {
|
||||
$structure = new FluentStructure();
|
||||
}
|
||||
$this->structure = $structure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SELECT query from $table
|
||||
*
|
||||
* @param string $table - db table name
|
||||
* @param integer $primaryKey - return one row by primary key
|
||||
*
|
||||
* @return \SelectQuery
|
||||
*/
|
||||
public function from($table, $primaryKey = null) {
|
||||
$query = new SelectQuery($this, $table);
|
||||
if ($primaryKey !== null) {
|
||||
$tableTable = $query->getFromTable();
|
||||
$tableAlias = $query->getFromAlias();
|
||||
$primaryKeyName = $this->structure->getPrimaryKey($tableTable);
|
||||
$query = $query->where("$tableAlias.$primaryKeyName", $primaryKey);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create INSERT INTO query
|
||||
*
|
||||
* @param string $table
|
||||
* @param array $values - accepts one or multiple rows, @see docs
|
||||
*
|
||||
* @return \InsertQuery
|
||||
*/
|
||||
public function insertInto($table, $values = array()) {
|
||||
$query = new InsertQuery($this, $table, $values);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create UPDATE query
|
||||
*
|
||||
* @param string $table
|
||||
* @param array|string $set
|
||||
* @param string $primaryKey
|
||||
*
|
||||
* @return \UpdateQuery
|
||||
*/
|
||||
public function update($table, $set = array(), $primaryKey = null) {
|
||||
$query = new UpdateQuery($this, $table);
|
||||
$query->set($set);
|
||||
if ($primaryKey) {
|
||||
$primaryKeyName = $this->getStructure()->getPrimaryKey($table);
|
||||
$query = $query->where($primaryKeyName, $primaryKey);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DELETE query
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $primaryKey delete only row by primary key
|
||||
*
|
||||
* @return \DeleteQuery
|
||||
*/
|
||||
public function delete($table, $primaryKey = null) {
|
||||
$query = new DeleteQuery($this, $table);
|
||||
if ($primaryKey) {
|
||||
$primaryKeyName = $this->getStructure()->getPrimaryKey($table);
|
||||
$query = $query->where($primaryKeyName, $primaryKey);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DELETE FROM query
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $primaryKey
|
||||
*
|
||||
* @return \DeleteQuery
|
||||
*/
|
||||
public function deleteFrom($table, $primaryKey = null) {
|
||||
$args = func_get_args();
|
||||
|
||||
return call_user_func_array(array($this, 'delete'), $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \PDO
|
||||
*/
|
||||
public function getPdo() {
|
||||
return $this->pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \FluentStructure
|
||||
*/
|
||||
public function getStructure() {
|
||||
return $this->structure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the \PDO connection to the database
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function close() {
|
||||
$this->pdo = null;
|
||||
}
|
||||
|
||||
}
|
60
vendor/FluentPDO/FluentStructure.php
vendored
Normal file
60
vendor/FluentPDO/FluentStructure.php
vendored
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class FluentStructure
|
||||
*/
|
||||
class FluentStructure
|
||||
{
|
||||
|
||||
/** @var string */
|
||||
private $primaryKey;
|
||||
/** @var string */
|
||||
private $foreignKey;
|
||||
|
||||
/**
|
||||
* FluentStructure constructor.
|
||||
*
|
||||
* @param string $primaryKey
|
||||
* @param string $foreignKey
|
||||
*/
|
||||
function __construct($primaryKey = 'id', $foreignKey = '%s_id') {
|
||||
if ($foreignKey === null) {
|
||||
$foreignKey = $primaryKey;
|
||||
}
|
||||
$this->primaryKey = $primaryKey;
|
||||
$this->foreignKey = $foreignKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $table
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPrimaryKey($table) {
|
||||
return $this->key($this->primaryKey, $table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $table
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getForeignKey($table) {
|
||||
return $this->key($this->foreignKey, $table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|callback $key
|
||||
* @param string $table
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function key($key, $table) {
|
||||
if (is_callable($key)) {
|
||||
return $key($table);
|
||||
}
|
||||
|
||||
return sprintf($key, $table);
|
||||
}
|
||||
|
||||
}
|
86
vendor/FluentPDO/FluentUtils.php
vendored
Normal file
86
vendor/FluentPDO/FluentUtils.php
vendored
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class FluentUtils
|
||||
*/
|
||||
class FluentUtils
|
||||
{
|
||||
|
||||
/**
|
||||
* Convert "camelCaseWord" to "CAMEL CASE WORD"
|
||||
*
|
||||
* @param string $string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function toUpperWords($string) {
|
||||
return trim(strtoupper(preg_replace('/(.)([A-Z]+)/', '$1 $2', $string)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function formatQuery($query) {
|
||||
$query = preg_replace(
|
||||
'/WHERE|FROM|GROUP BY|HAVING|ORDER BY|LIMIT|OFFSET|UNION|ON DUPLICATE KEY UPDATE|VALUES/',
|
||||
"\n$0", $query
|
||||
);
|
||||
$query = preg_replace(
|
||||
'/INNER|LEFT|RIGHT|CASE|WHEN|END|ELSE|AND/',
|
||||
"\n $0", $query
|
||||
);
|
||||
// remove trailing spaces
|
||||
$query = preg_replace("/\s+\n/", "\n", $query);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts columns from strings to types according to
|
||||
* PDOStatement::columnMeta
|
||||
* http://stackoverflow.com/a/9952703/3006989
|
||||
*
|
||||
* @param PDOStatement $st
|
||||
* @param array $assoc returned by PDOStatement::fetch with PDO::FETCH_ASSOC
|
||||
* @return copy of $assoc with matching type fields
|
||||
*/
|
||||
public static function convertToNativeTypes(PDOStatement $statement, $rows)
|
||||
{
|
||||
for ($i = 0; $columnMeta = $statement->getColumnMeta($i); $i++)
|
||||
{
|
||||
$type = $columnMeta['native_type'];
|
||||
|
||||
switch($type)
|
||||
{
|
||||
case 'DECIMAL':
|
||||
case 'TINY':
|
||||
case 'SHORT':
|
||||
case 'LONG':
|
||||
case 'LONGLONG':
|
||||
case 'INT24':
|
||||
if(isset($rows[$columnMeta['name']])){
|
||||
$rows[$columnMeta['name']] = $rows[$columnMeta['name']] + 0;
|
||||
}else{
|
||||
if(is_array($rows) || $rows instanceof Traversable){
|
||||
foreach($rows as &$row){
|
||||
if(isset($row[$columnMeta['name']])){
|
||||
$row[$columnMeta['name']] = $row[$columnMeta['name']] + 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'DATETIME':
|
||||
case 'DATE':
|
||||
case 'TIMESTAMP':
|
||||
// convert to date type?
|
||||
break;
|
||||
// default: keep as string
|
||||
}
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
}
|
221
vendor/FluentPDO/InsertQuery.php
vendored
Normal file
221
vendor/FluentPDO/InsertQuery.php
vendored
Normal file
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
|
||||
/** INSERT query builder
|
||||
*/
|
||||
class InsertQuery extends BaseQuery
|
||||
{
|
||||
|
||||
/** @var array */
|
||||
private $columns = array();
|
||||
|
||||
/** @var array */
|
||||
private $firstValue = array();
|
||||
|
||||
/** @var bool */
|
||||
private $ignore = false;
|
||||
/** @var bool */
|
||||
private $delayed = false;
|
||||
|
||||
/**
|
||||
* InsertQuery constructor.
|
||||
*
|
||||
* @param FluentPDO $fpdo
|
||||
* @param string $table
|
||||
* @param $values
|
||||
*/
|
||||
public function __construct(FluentPDO $fpdo, $table, $values) {
|
||||
$clauses = array(
|
||||
'INSERT INTO' => array($this, 'getClauseInsertInto'),
|
||||
'VALUES' => array($this, 'getClauseValues'),
|
||||
'ON DUPLICATE KEY UPDATE' => array($this, 'getClauseOnDuplicateKeyUpdate'),
|
||||
);
|
||||
parent::__construct($fpdo, $clauses);
|
||||
|
||||
$this->statements['INSERT INTO'] = $table;
|
||||
$this->values($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute insert query
|
||||
*
|
||||
* @param mixed $sequence
|
||||
*
|
||||
* @return integer last inserted id or false
|
||||
*/
|
||||
public function execute($sequence = null) {
|
||||
$result = parent::execute();
|
||||
if ($result) {
|
||||
return $this->getPDO()->lastInsertId($sequence);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ON DUPLICATE KEY UPDATE
|
||||
*
|
||||
* @param array $values
|
||||
*
|
||||
* @return \InsertQuery
|
||||
*/
|
||||
public function onDuplicateKeyUpdate($values) {
|
||||
$this->statements['ON DUPLICATE KEY UPDATE'] = array_merge(
|
||||
$this->statements['ON DUPLICATE KEY UPDATE'], $values
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add VALUES
|
||||
*
|
||||
* @param $values
|
||||
*
|
||||
* @return \InsertQuery
|
||||
* @throws Exception
|
||||
*/
|
||||
public function values($values) {
|
||||
if (!is_array($values)) {
|
||||
throw new Exception('Param VALUES for INSERT query must be array');
|
||||
}
|
||||
$first = current($values);
|
||||
if (is_string(key($values))) {
|
||||
// is one row array
|
||||
$this->addOneValue($values);
|
||||
} elseif (is_array($first) && is_string(key($first))) {
|
||||
// this is multi values
|
||||
foreach ($values as $oneValue) {
|
||||
$this->addOneValue($oneValue);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force insert operation to fail silently
|
||||
*
|
||||
* @return \InsertQuery
|
||||
*/
|
||||
public function ignore() {
|
||||
$this->ignore = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** Force insert operation delay support
|
||||
*
|
||||
* @return \InsertQuery
|
||||
*/
|
||||
public function delayed() {
|
||||
$this->delayed = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getClauseInsertInto() {
|
||||
return 'INSERT' . ($this->ignore ? " IGNORE" : '') . ($this->delayed ? " DELAYED" : '') . ' INTO ' . $this->statements['INSERT INTO'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $param
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function parameterGetValue($param) {
|
||||
return $param instanceof FluentLiteral ? (string)$param : '?';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getClauseValues() {
|
||||
$valuesArray = array();
|
||||
foreach ($this->statements['VALUES'] as $rows) {
|
||||
// literals should not be parametrized.
|
||||
// They are commonly used to call engine functions or literals.
|
||||
// Eg: NOW(), CURRENT_TIMESTAMP etc
|
||||
$placeholders = array_map(array($this, 'parameterGetValue'), $rows);
|
||||
$valuesArray[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
|
||||
$columns = implode(', ', $this->columns);
|
||||
$values = implode(', ', $valuesArray);
|
||||
|
||||
return " ($columns) VALUES $values";
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all FluentLiteral instances from the argument
|
||||
* since they are not to be used as PDO parameters but rather injected directly into the query
|
||||
*
|
||||
* @param $statements
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function filterLiterals($statements) {
|
||||
$f = function ($item) {
|
||||
return !$item instanceof FluentLiteral;
|
||||
};
|
||||
|
||||
return array_map(function ($item) use ($f) {
|
||||
if (is_array($item)) {
|
||||
return array_filter($item, $f);
|
||||
}
|
||||
|
||||
return $item;
|
||||
}, array_filter($statements, $f));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function buildParameters() {
|
||||
$this->parameters = array_merge(
|
||||
$this->filterLiterals($this->statements['VALUES']),
|
||||
$this->filterLiterals($this->statements['ON DUPLICATE KEY UPDATE'])
|
||||
);
|
||||
|
||||
return parent::buildParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getClauseOnDuplicateKeyUpdate() {
|
||||
$result = array();
|
||||
foreach ($this->statements['ON DUPLICATE KEY UPDATE'] as $key => $value) {
|
||||
$result[] = "$key = " . $this->parameterGetValue($value);
|
||||
}
|
||||
|
||||
return ' ON DUPLICATE KEY UPDATE ' . implode(', ', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $oneValue
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function addOneValue($oneValue) {
|
||||
// check if all $keys are strings
|
||||
foreach ($oneValue as $key => $value) {
|
||||
if (!is_string($key)) {
|
||||
throw new Exception('INSERT query: All keys of value array have to be strings.');
|
||||
}
|
||||
}
|
||||
if (!$this->firstValue) {
|
||||
$this->firstValue = $oneValue;
|
||||
}
|
||||
if (!$this->columns) {
|
||||
$this->columns = array_keys($oneValue);
|
||||
}
|
||||
if ($this->columns != array_keys($oneValue)) {
|
||||
throw new Exception('INSERT query: All VALUES have to same keys (columns).');
|
||||
}
|
||||
$this->statements['VALUES'][] = $oneValue;
|
||||
}
|
||||
|
||||
}
|
194
vendor/FluentPDO/SelectQuery.php
vendored
Normal file
194
vendor/FluentPDO/SelectQuery.php
vendored
Normal file
|
@ -0,0 +1,194 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* SELECT query builder
|
||||
*
|
||||
* @method SelectQuery select(string $column) add one or more columns in SELECT to query
|
||||
* @method SelectQuery leftJoin(string $statement) add LEFT JOIN to query
|
||||
* ($statement can be 'table' name only or 'table:' means back reference)
|
||||
* @method SelectQuery innerJoin(string $statement) add INNER JOIN to query
|
||||
* ($statement can be 'table' name only or 'table:' means back reference)
|
||||
* @method SelectQuery groupBy(string $column) add GROUP BY to query
|
||||
* @method SelectQuery having(string $column) add HAVING query
|
||||
* @method SelectQuery orderBy(string $column) add ORDER BY to query
|
||||
* @method SelectQuery limit(int $limit) add LIMIT to query
|
||||
* @method SelectQuery offset(int $offset) add OFFSET to query
|
||||
*/
|
||||
class SelectQuery extends CommonQuery implements Countable
|
||||
{
|
||||
|
||||
/** @var mixed */
|
||||
private $fromTable;
|
||||
/** @var mixed */
|
||||
private $fromAlias;
|
||||
|
||||
/** @var boolean */
|
||||
private $convertTypes = false;
|
||||
|
||||
/**
|
||||
* SelectQuery constructor.
|
||||
*
|
||||
* @param FluentPDO $fpdo
|
||||
* @param $from
|
||||
*/
|
||||
function __construct(FluentPDO $fpdo, $from) {
|
||||
$clauses = array(
|
||||
'SELECT' => ', ',
|
||||
'FROM' => null,
|
||||
'JOIN' => array($this, 'getClauseJoin'),
|
||||
'WHERE' => ' AND ',
|
||||
'GROUP BY' => ',',
|
||||
'HAVING' => ' AND ',
|
||||
'ORDER BY' => ', ',
|
||||
'LIMIT' => null,
|
||||
'OFFSET' => null,
|
||||
"\n--" => "\n--",
|
||||
);
|
||||
parent::__construct($fpdo, $clauses);
|
||||
|
||||
// initialize statements
|
||||
$fromParts = explode(' ', $from);
|
||||
$this->fromTable = reset($fromParts);
|
||||
$this->fromAlias = end($fromParts);
|
||||
|
||||
$this->statements['FROM'] = $from;
|
||||
$this->statements['SELECT'][] = $this->fromAlias . '.*';
|
||||
$this->joins[] = $this->fromAlias;
|
||||
|
||||
if(isset($fpdo->convertTypes) && $fpdo->convertTypes){
|
||||
$this->convertTypes = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Return table name from FROM clause
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getFromTable() {
|
||||
return $this->fromTable;
|
||||
}
|
||||
|
||||
/** Return table alias from FROM clause
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getFromAlias() {
|
||||
return $this->fromAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single column
|
||||
*
|
||||
* @param int $columnNumber
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function fetchColumn($columnNumber = 0) {
|
||||
if (($s = $this->execute()) !== false) {
|
||||
return $s->fetchColumn($columnNumber);
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch first row or column
|
||||
*
|
||||
* @param string $column column name or empty string for the whole row
|
||||
*
|
||||
* @return mixed string, array or false if there is no row
|
||||
*/
|
||||
public function fetch($column = '') {
|
||||
$s = $this->execute();
|
||||
if ($s === false) {
|
||||
return false;
|
||||
}
|
||||
$row = $s->fetch();
|
||||
|
||||
if($this->convertTypes){
|
||||
$row = FluentUtils::convertToNativeTypes($s,$row);
|
||||
}
|
||||
|
||||
if ($row && $column != '') {
|
||||
if (is_object($row)) {
|
||||
return $row->{$column};
|
||||
} else {
|
||||
return $row[$column];
|
||||
}
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch pairs
|
||||
*
|
||||
* @param $key
|
||||
* @param $value
|
||||
* @param $object
|
||||
*
|
||||
* @return array of fetched rows as pairs
|
||||
*/
|
||||
public function fetchPairs($key, $value, $object = false) {
|
||||
if (($s = $this->select(null)->select("$key, $value")->asObject($object)->execute()) !== false) {
|
||||
return $s->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
/** Fetch all row
|
||||
*
|
||||
* @param string $index specify index column
|
||||
* @param string $selectOnly select columns which could be fetched
|
||||
*
|
||||
* @return array of fetched rows
|
||||
*/
|
||||
public function fetchAll($index = '', $selectOnly = '') {
|
||||
if ($selectOnly) {
|
||||
$this->select(null)->select($index . ', ' . $selectOnly);
|
||||
}
|
||||
if ($index) {
|
||||
$data = array();
|
||||
foreach ($this as $row) {
|
||||
if (is_object($row)) {
|
||||
$data[$row->{$index}] = $row;
|
||||
} else {
|
||||
$data[$row[$index]] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
} else {
|
||||
if (($s = $this->execute()) !== false) {
|
||||
if($this->convertTypes){
|
||||
return FluentUtils::convertToNativeTypes($s,$s->fetchAll());
|
||||
}else{
|
||||
return $s->fetchAll();
|
||||
}
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Countable interface doesn't break current \FluentPDO select query
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count() {
|
||||
$fpdo = clone $this;
|
||||
|
||||
return (int)$fpdo->select(null)->select('COUNT(*)')->fetchColumn();
|
||||
}
|
||||
|
||||
public function getIterator() {
|
||||
if($this->convertTypes){
|
||||
return new ArrayIterator($this->fetchAll());
|
||||
}else{
|
||||
return $this->execute();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
107
vendor/FluentPDO/UpdateQuery.php
vendored
Normal file
107
vendor/FluentPDO/UpdateQuery.php
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
/** UPDATE query builder
|
||||
*
|
||||
* @method UpdateQuery leftJoin(string $statement) add LEFT JOIN to query
|
||||
* ($statement can be 'table' name only or 'table:' means back reference)
|
||||
* @method UpdateQuery innerJoin(string $statement) add INNER JOIN to query
|
||||
* ($statement can be 'table' name only or 'table:' means back reference)
|
||||
* @method UpdateQuery orderBy(string $column) add ORDER BY to query
|
||||
* @method UpdateQuery limit(int $limit) add LIMIT to query
|
||||
*/
|
||||
class UpdateQuery extends CommonQuery
|
||||
{
|
||||
|
||||
/**
|
||||
* UpdateQuery constructor.
|
||||
*
|
||||
* @param FluentPDO $fpdo
|
||||
* @param $table
|
||||
*/
|
||||
public function __construct(FluentPDO $fpdo, $table) {
|
||||
$clauses = array(
|
||||
'UPDATE' => array($this, 'getClauseUpdate'),
|
||||
'JOIN' => array($this, 'getClauseJoin'),
|
||||
'SET' => array($this, 'getClauseSet'),
|
||||
'WHERE' => ' AND ',
|
||||
'ORDER BY' => ', ',
|
||||
'LIMIT' => null,
|
||||
);
|
||||
parent::__construct($fpdo, $clauses);
|
||||
|
||||
$this->statements['UPDATE'] = $table;
|
||||
|
||||
$tableParts = explode(' ', $table);
|
||||
$this->joins[] = end($tableParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array $fieldOrArray
|
||||
* @param bool|string $value
|
||||
*
|
||||
* @return $this
|
||||
* @throws Exception
|
||||
*/
|
||||
public function set($fieldOrArray, $value = false) {
|
||||
if (!$fieldOrArray) {
|
||||
return $this;
|
||||
}
|
||||
if (is_string($fieldOrArray) && $value !== false) {
|
||||
$this->statements['SET'][$fieldOrArray] = $value;
|
||||
} else {
|
||||
if (!is_array($fieldOrArray)) {
|
||||
throw new Exception('You must pass a value, or provide the SET list as an associative array. column => value');
|
||||
} else {
|
||||
foreach ($fieldOrArray as $field => $value) {
|
||||
$this->statements['SET'][$field] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute update query
|
||||
*
|
||||
* @param boolean $getResultAsPdoStatement true to return the pdo statement instead of row count
|
||||
*
|
||||
* @return int|boolean|\PDOStatement
|
||||
*/
|
||||
public function execute($getResultAsPdoStatement = false) {
|
||||
$result = parent::execute();
|
||||
if ($getResultAsPdoStatement) {
|
||||
return $result;
|
||||
}
|
||||
if ($result) {
|
||||
return $result->rowCount();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getClauseUpdate() {
|
||||
return 'UPDATE ' . $this->statements['UPDATE'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getClauseSet() {
|
||||
$setArray = array();
|
||||
foreach ($this->statements['SET'] as $field => $value) {
|
||||
if ($value instanceof FluentLiteral) {
|
||||
$setArray[] = $field . ' = ' . $value;
|
||||
} else {
|
||||
$setArray[] = $field . ' = ?';
|
||||
$this->parameters['SET'][$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return ' SET ' . implode(', ', $setArray);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue