diff --git a/application/admin/controller/Form.php b/application/admin/controller/Form.php index af245feb..dc68a628 100644 --- a/application/admin/controller/Form.php +++ b/application/admin/controller/Form.php @@ -12,13 +12,20 @@ use app\common\controller\Admin; class Form extends Admin { + public function _initialize(){ + parent::_initialize(); + $this->model = model('Form'); + } + //自定义表单 public function index(){ - $list = array(); + $map = array(); + $order = "id desc"; + $list = $this->model->where($map)->order($order)->paginate(25); $data = array( 'list' => $list, - //'page' => $list->render() + 'page' => $list->render() ); $this->setMeta('自定义表单'); $this->assign($data); @@ -59,4 +66,15 @@ class Form extends Admin { return $this->error('删除失败!'); } } + + /** + * @title 表单数据 + * @description 表单数据 + * @Author molong + * @DateTime 2017-06-30 + * @return html 页面 + */ + public function lists(){ + return $this->fetch(); + } } \ No newline at end of file diff --git a/application/api/controller/Wechat.php b/application/api/controller/Wechat.php new file mode 100644 index 00000000..1e09a6f5 --- /dev/null +++ b/application/api/controller/Wechat.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- + +namespace app\api\controller; +use app\common\controller\Api; + +class Wechat extends Api { + + public function config(){ + $js = new \Wechat\WechatScript(config('wxapp')); + $this->data['data'] = $js->getJsSign('http://test.tensent.cn:81/home'); +// dump($this->data); +// $this->data['data'] = array( +// 'appid' => 'dddd', +// 'timestamp' => time() +// ); + return $this->data; + } +} \ No newline at end of file diff --git a/application/common/controller/Api.php b/application/common/controller/Api.php index 727a4228..90035d21 100644 --- a/application/common/controller/Api.php +++ b/application/common/controller/Api.php @@ -14,6 +14,7 @@ class Api { protected $data; public function __construct() { + header("Access-Control-Allow-Origin: *"); $this->data = array('code' => 0, 'msg' => '', 'time' => time(), 'data' => ''); } } \ No newline at end of file diff --git a/application/common/model/Form.php b/application/common/model/Form.php index e6858ada..8daaeedd 100644 --- a/application/common/model/Form.php +++ b/application/common/model/Form.php @@ -14,4 +14,64 @@ namespace app\common\model; */ class Form extends Base{ + protected $auto = ['update_time']; + protected $insert = ['name', 'create_time', 'status' => 1, 'list_grid'=>"id:ID\r\ntitle:标题\r\ncreate_time:添加时间|time_format\r\nupdate_time:更新时间|time_format"]; + protected $type = array( + 'id' => 'integer', + 'create_time' => 'integer', + 'update_time' => 'integer', + ); + + + protected static function init(){ + self::beforeInsert(function($event){ + $data = $event->toArray(); + $tablename = strtolower($data['name']); + //实例化一个数据库操作类 + $db = new \com\Datatable(); + //检查表是否存在并创建 + if (!$db->CheckTable($tablename)) { + //创建新表 + return $db->initTable($tablename, $data['title'], 'id')->query(); + }else{ + return false; + } + }); + self::afterInsert(function($event){ + $data = $event->toArray(); + + $fields = include(APP_PATH.'admin/fields.php'); + if (!empty($fields)) { + foreach ($fields as $key => $value) { + if ($data['is_doc']) { + $fields[$key]['form_id'] = $data['id']; + }else{ + if (in_array($key, array('uid', 'status', 'view', 'create_time', 'update_time'))) { + $fields[$key]['form_id'] = $data['id']; + }else{ + unset($fields[$key]); + } + } + } + model('FormAttr')->saveAll($fields); + } + return true; + }); + self::beforeUpdate(function($event){ + $data = $event->toArray(); + if (isset($data['attribute_sort']) && $data['attribute_sort']) { + $attribute_sort = json_decode($data['attribute_sort'], true); + + if (!empty($attribute_sort)) { + foreach ($attribute_sort as $key => $value) { + db('FormAttr')->where('id', 'IN', $value)->setField('group_id', $key); + foreach ($value as $k => $v) { + db('FormAttr')->where('id', $v)->setField('sort', $k); + } + } + } + } + return true; + }); + } } \ No newline at end of file diff --git a/application/config.php b/application/config.php index b3d34b71..8e67e94e 100644 --- a/application/config.php +++ b/application/config.php @@ -107,5 +107,9 @@ return array( 'view_replace_str' => array( '__ADDONS__' => BASE_PATH . '/addons', '__PUBLIC__' => BASE_PATH . '/public', + ), + 'wxapp' => array( + 'appid'=>'wx4924a63b43e2fc1a', + 'appsecret'=>'0821fc43d2305d4b4722a591361df438' ) ); \ No newline at end of file diff --git a/core/base.php b/core/base.php index 9744f01d..31a2333f 100644 --- a/core/base.php +++ b/core/base.php @@ -9,7 +9,7 @@ // | Author: liu21st // +---------------------------------------------------------------------- -define('THINK_VERSION', '5.0.9'); +define('THINK_VERSION', '5.0.10'); define('THINK_START_TIME', microtime(true)); define('THINK_START_MEM', memory_get_usage()); define('EXT', '.php'); diff --git a/core/console.php b/core/console.php new file mode 100644 index 00000000..578e4a7c --- /dev/null +++ b/core/console.php @@ -0,0 +1,20 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +// ThinkPHP 引导文件 +// 加载基础文件 +require __DIR__ . '/base.php'; + +// 执行应用 +App::initCommon(); +Console::init(); diff --git a/core/convention.php b/core/convention.php index 801c90a8..4bde7d88 100644 --- a/core/convention.php +++ b/core/convention.php @@ -7,7 +7,7 @@ return [ // 默认Host地址 'app_host' => '', // 应用调试模式 - 'app_debug' => true, + 'app_debug' => false, // 应用Trace 'app_trace' => false, // 应用模式状态 @@ -74,6 +74,8 @@ return [ 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], // pathinfo分隔符 'pathinfo_depr' => '/', + // HTTPS代理标识 + 'https_agent_name' => '', // URL伪静态后缀 'url_html_suffix' => 'html', // URL普通方式参数 用于自动生成 diff --git a/core/extend/Wechat/Cache/20170703.log b/core/extend/Wechat/Cache/20170703.log new file mode 100644 index 00000000..73fde22a --- /dev/null +++ b/core/extend/Wechat/Cache/20170703.log @@ -0,0 +1,3 @@ +[2017/07/03 16:00:58] MSG - Get New AccessToken Success. +[2017/07/03 18:02:14] MSG - Get New AccessToken Success. +[2017/07/03 22:47:05] MSG - Get New AccessToken Success. diff --git a/core/extend/Wechat/Cache/wechat_access_token_wx4924a63b43e2fc1a b/core/extend/Wechat/Cache/wechat_access_token_wx4924a63b43e2fc1a new file mode 100644 index 00000000..310cf079 --- /dev/null +++ b/core/extend/Wechat/Cache/wechat_access_token_wx4924a63b43e2fc1a @@ -0,0 +1 @@ +a:2:{s:5:"value";s:138:"hX91IMjz-VD6-uRvXuhwuQb47YPpPKVZ0LQu_ToussTNxywjwyzt26ROt5_wFsRRoGfE9kdLffZmZFgiGf_xRaFHfka8aYQvknDUWXKr5g5lGg84tCKMjCifMBiRjSTkPYYdAGAUDD";s:7:"expired";i:1499098225;} \ No newline at end of file diff --git a/core/extend/Wechat/Cache/wechat_jsapi_ticket_wx4924a63b43e2fc1a b/core/extend/Wechat/Cache/wechat_jsapi_ticket_wx4924a63b43e2fc1a new file mode 100644 index 00000000..294c8997 --- /dev/null +++ b/core/extend/Wechat/Cache/wechat_jsapi_ticket_wx4924a63b43e2fc1a @@ -0,0 +1 @@ +a:2:{s:5:"value";s:86:"sM4AOVdWfPE4DxkXGEs8VIMcGjJAQX643GrxVbHHaF1crt_AsDafYIdQTn-FW5wCE3l-EU6K8o2ylaud4-1BAA";s:7:"expired";i:1499100325;} \ No newline at end of file diff --git a/core/extend/Wechat/Lib/Cache.php b/core/extend/Wechat/Lib/Cache.php new file mode 100644 index 00000000..43321bf6 --- /dev/null +++ b/core/extend/Wechat/Lib/Cache.php @@ -0,0 +1,93 @@ + + * @date 2016-08-20 17:50 + */ +class Cache { + + /** + * 缓存位置 + * @var string + */ + static public $cachepath; + + /** + * 设置缓存 + * @param string $name + * @param string $value + * @param int $expired + * @return mixed + */ + static public function set($name, $value, $expired = 0) { + if (isset(Loader::$callback['CacheSet'])) { + return call_user_func_array(Loader::$callback['CacheSet'], func_get_args()); + } + $data = serialize(array('value' => $value, 'expired' => $expired > 0 ? time() + $expired : 0)); + return self::check() && file_put_contents(self::$cachepath . $name, $data); + } + + /** + * 读取缓存 + * @param string $name + * @return mixed + */ + static public function get($name) { + if (isset(Loader::$callback['CacheGet'])) { + return call_user_func_array(Loader::$callback['CacheGet'], func_get_args()); + } + if (self::check() && ($file = self::$cachepath . $name) && file_exists($file) && ($data = file_get_contents($file)) && !empty($data)) { + $data = unserialize($data); + if (isset($data['expired']) && ($data['expired'] > time() || $data['expired'] === 0)) { + return isset($data['value']) ? $data['value'] : null; + } + } + return null; + } + + /** + * 删除缓存 + * @param string $name + * @return mixed + */ + static public function del($name) { + if (isset(Loader::$callback['CacheDel'])) { + return call_user_func_array(Loader::$callback['CacheDel'], func_get_args()); + } + return self::check() && @unlink(self::$cachepath . $name); + } + + /** + * 输出内容到日志 + * @param string $line + * @param string $filename + * @return mixed + */ + static public function put($line, $filename = '') { + if (isset(Loader::$callback['CachePut'])) { + return call_user_func_array(Loader::$callback['CachePut'], func_get_args()); + } + empty($filename) && $filename = date('Ymd') . '.log'; + return self::check() && file_put_contents(self::$cachepath . $filename, '[' . date('Y/m/d H:i:s') . "] {$line}\n", FILE_APPEND); + } + + /** + * 检查缓存目录 + * @return bool + */ + static protected function check() { + empty(self::$cachepath) && self::$cachepath = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Cache' . DIRECTORY_SEPARATOR; + self::$cachepath = rtrim(self::$cachepath, '/\\') . DIRECTORY_SEPARATOR; + if (!is_dir(self::$cachepath) && !mkdir(self::$cachepath, 0755, TRUE)) { + return FALSE; + } + return TRUE; + } + +} diff --git a/core/extend/Wechat/Lib/Common.php b/core/extend/Wechat/Lib/Common.php new file mode 100644 index 00000000..d0af0d68 --- /dev/null +++ b/core/extend/Wechat/Lib/Common.php @@ -0,0 +1,179 @@ + + * @date 2016/05/28 11:55 + */ +class Common { + + /** API接口URL需要使用此前缀 */ + const API_BASE_URL_PREFIX = 'https://api.weixin.qq.com'; + const API_URL_PREFIX = 'https://api.weixin.qq.com/cgi-bin'; + const GET_TICKET_URL = '/ticket/getticket?'; + const AUTH_URL = '/token?grant_type=client_credential&'; + public $token; + public $encodingAesKey; + public $encrypt_type; + public $appid; + public $appsecret; + public $access_token; + public $postxml; + public $_msg; + public $errCode = 0; + public $errMsg = ""; + public $config = array(); + private $_retry = FALSE; + + /** + * 构造方法 + * @param array $options + */ + public function __construct($options = array()) { + $config = Loader::config($options); + $this->token = isset($config['token']) ? $config['token'] : ''; + $this->appid = isset($config['appid']) ? $config['appid'] : ''; + $this->appsecret = isset($config['appsecret']) ? $config['appsecret'] : ''; + $this->encodingAesKey = isset($config['encodingaeskey']) ? $config['encodingaeskey'] : ''; + $this->config = $config; + } + + /** + * 接口验证 + * @return bool + */ + public function valid() { + $encryptStr = ""; + if ($_SERVER['REQUEST_METHOD'] == "POST") { + $postStr = file_get_contents("php://input"); + $array = (array)simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA); + $this->encrypt_type = isset($_GET["encrypt_type"]) ? $_GET["encrypt_type"] : ''; + if ($this->encrypt_type == 'aes') { + $encryptStr = $array['Encrypt']; + !class_exists('Prpcrypt', FALSE) && require __DIR__ . '/Prpcrypt.php'; + $pc = new Prpcrypt($this->encodingAesKey); + $array = $pc->decrypt($encryptStr, $this->appid); + if (!isset($array[0]) || intval($array[0]) > 0) { + $this->errCode = $array[0]; + $this->errMsg = $array[1]; + Tools::log("Interface Authentication Failed. {$this->errMsg}[{$this->errCode}]", 'ERR'); + return false; + } + $this->postxml = $array[1]; + empty($this->appid) && $this->appid = $array[2]; + } else { + $this->postxml = $postStr; + } + } elseif (isset($_GET["echostr"])) { + if ($this->checkSignature()) { + exit($_GET["echostr"]); + } else { + return false; + } + } + if (!$this->checkSignature($encryptStr)) { + $this->errMsg = 'Interface authentication failed, please use the correct method to call.'; + return false; + } + return true; + } + + /** + * 验证来自微信服务器 + * @param string $str + * @return bool + */ + private function checkSignature($str = '') { + // 如果存在加密验证则用加密验证段 + $signature = isset($_GET["msg_signature"]) ? $_GET["msg_signature"] : (isset($_GET["signature"]) ? $_GET["signature"] : ''); + $timestamp = isset($_GET["timestamp"]) ? $_GET["timestamp"] : ''; + $nonce = isset($_GET["nonce"]) ? $_GET["nonce"] : ''; + $tmpArr = array($this->token, $timestamp, $nonce, $str); + sort($tmpArr, SORT_STRING); + if (sha1(implode($tmpArr)) == $signature) { + return true; + } else { + return false; + } + } + + /** + * 获取公众号访问 access_token + * @param string $appid 如在类初始化时已提供,则可为空 + * @param string $appsecret 如在类初始化时已提供,则可为空 + * @param string $token 手动指定access_token,非必要情况不建议用 + * @return bool|string + */ + public function getAccessToken($appid = '', $appsecret = '', $token = '') { + if (!$appid || !$appsecret) { + $appid = $this->appid; + $appsecret = $this->appsecret; + } + if ($token) { + return $this->access_token = $token; + } + $cache = 'wechat_access_token_' . $appid; + if (($access_token = Tools::getCache($cache)) && !empty($access_token)) { + return $this->access_token = $access_token; + } + # 检测事件注册 + if (isset(Loader::$callback[__FUNCTION__])) { + return $this->access_token = call_user_func_array(Loader::$callback[__FUNCTION__], array(&$this, &$cache)); + } + $result = Tools::httpGet(self::API_URL_PREFIX . self::AUTH_URL . 'appid=' . $appid . '&secret=' . $appsecret); + if ($result) { + $json = json_decode($result, true); + if (!$json || isset($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + Tools::log("Get New AccessToken Error. {$this->errMsg}[{$this->errCode}]", 'ERR'); + return false; + } + $this->access_token = $json['access_token']; + Tools::log("Get New AccessToken Success."); + Tools::setCache($cache, $this->access_token, 5000); + return $this->access_token; + } + return false; + } + + /** + * 接口失败重试 + * @param $method SDK方法名称 + * @param array $arguments SDK方法参数 + * @return bool|mixed + */ + protected function checkRetry($method, $arguments = array()) { + if (!$this->_retry && in_array($this->errCode, array('40014', '40001', '41001', '42001'))) { + Tools::log("Run {$method} Faild. {$this->errMsg}[{$this->errCode}]", 'ERR'); + ($this->_retry = true) && $this->resetAuth(); + $this->errCode = 40001; + $this->errMsg = 'no access'; + Tools::log("Retry Run {$method} ..."); + return call_user_func_array(array($this, $method), $arguments); + } + return false; + } + + /** + * 删除验证数据 + * @param string $appid 如在类初始化时已提供,则可为空 + * @return bool + */ + public function resetAuth($appid = '') { + $authname = 'wechat_access_token_' . (empty($appid) ? $this->appid : $appid); + Tools::log("Reset Auth And Remove Old AccessToken."); + $this->access_token = ''; + Tools::removeCache($authname); + return true; + } + +} diff --git a/core/extend/Wechat/Lib/Prpcrypt.php b/core/extend/Wechat/Lib/Prpcrypt.php new file mode 100644 index 00000000..07404620 --- /dev/null +++ b/core/extend/Wechat/Lib/Prpcrypt.php @@ -0,0 +1,176 @@ + PKCS7Encoder::$block_size) { + $pad = 0; + } + return substr($text, 0, (strlen($text) - $pad)); + } + +} + +/** + * 接收和推送给公众平台消息的加解密 + * @category WechatSDK + * @subpackage library + * @date 2016/06/28 11:59 + */ +class Prpcrypt { + + public $key; + + function __construct($k) { + $this->key = base64_decode($k . "="); + } + + /** + * 对明文进行加密 + * @param string $text 需要加密的明文 + * @param string $appid 公众号APPID + * @return string 加密后的密文 + */ + public function encrypt($text, $appid) { + try { + //获得16位随机字符串,填充到明文之前 + $random = $this->getRandomStr();//"aaaabbbbccccdddd"; + $text = $random . pack("N", strlen($text)) . $text . $appid; + $iv = substr($this->key, 0, 16); + $pkc_encoder = new PKCS7Encoder; + $text = $pkc_encoder->encode($text); + $encrypted = openssl_encrypt($text, 'AES-256-CBC', substr($this->key, 0, 32), OPENSSL_ZERO_PADDING, $iv); + return array(ErrorCode::$OK, $encrypted); + } catch (Exception $e) { + return array(ErrorCode::$EncryptAESError, null); + } + } + + /** + * 对密文进行解密 + * @param string $encrypted 需要解密的密文 + * @param string $appid 公众号APPID + * @return string 解密得到的明文 + */ + public function decrypt($encrypted, $appid) { + try { + $iv = substr($this->key, 0, 16); + $decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', substr($this->key, 0, 32), OPENSSL_ZERO_PADDING, $iv); + } catch (Exception $e) { + return array(ErrorCode::$DecryptAESError, null); + } + try { + $pkc_encoder = new PKCS7Encoder; + $result = $pkc_encoder->decode($decrypted); + if (strlen($result) < 16) { + return ""; + } + $content = substr($result, 16, strlen($result)); + $len_list = unpack("N", substr($content, 0, 4)); + $xml_len = $len_list[1]; + $xml_content = substr($content, 4, $xml_len); + $from_appid = substr($content, $xml_len + 4); + if (!$appid) { + $appid = $from_appid; + } + } catch (Exception $e) { + return array(ErrorCode::$IllegalBuffer, null); + } + return array(0, $xml_content, $from_appid); + } + + /** + * 随机生成16位字符串 + * @return string 生成的字符串 + */ + function getRandomStr() { + $str = ""; + $str_pol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"; + $max = strlen($str_pol) - 1; + for ($i = 0; $i < 16; $i++) { + $str .= $str_pol[mt_rand(0, $max)]; + } + return $str; + } + +} + +/** + * 仅用作类内部使用 + * 不用于官方API接口的errCode码 + * Class ErrorCode + */ +class ErrorCode { + + public static $OK = 0; + public static $ValidateSignatureError = 40001; + public static $ParseXmlError = 40002; + public static $ComputeSignatureError = 40003; + public static $IllegalAesKey = 40004; + public static $ValidateAppidError = 40005; + public static $EncryptAESError = 40006; + public static $DecryptAESError = 40007; + public static $IllegalBuffer = 40008; + public static $EncodeBase64Error = 40009; + public static $DecodeBase64Error = 40010; + public static $GenReturnXmlError = 40011; + public static $errCode = array( + '0' => '处理成功', + '40001' => '校验签名失败', + '40002' => '解析xml失败', + '40003' => '计算签名失败', + '40004' => '不合法的AESKey', + '40005' => '校验AppID失败', + '40006' => 'AES加密失败', + '40007' => 'AES解密失败', + '40008' => '公众平台发送的xml不合法', + '40009' => 'Base64编码失败', + '40010' => 'Base64解码失败', + '40011' => '公众帐号生成回包xml失败' + ); + + /** + * 获取错误消息内容 + * @param string $err + * @return bool + */ + public static function getErrText($err) { + if (isset(self::$errCode[$err])) { + return self::$errCode[$err]; + } + return false; + } + +} diff --git a/core/extend/Wechat/Lib/Tools.php b/core/extend/Wechat/Lib/Tools.php new file mode 100644 index 00000000..d87d84ed --- /dev/null +++ b/core/extend/Wechat/Lib/Tools.php @@ -0,0 +1,268 @@ + + * @date 2016/05/28 11:55 + */ +class Tools { + + /** + * 产生随机字符串 + * @param int $length + * @param string $str + * @return string + */ + static public function createNoncestr($length = 32, $str = "") { + $chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + for ($i = 0; $i < $length; $i++) { + $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1); + } + return $str; + } + + /** + * 获取签名 + * @param array $arrdata 签名数组 + * @param string $method 签名方法 + * @return bool|string 签名值 + */ + static public function getSignature($arrdata, $method = "sha1") { + if (!function_exists($method)) { + return false; + } + ksort($arrdata); + $params = array(); + foreach ($arrdata as $key => $value) { + $params[] = "{$key}={$value}"; + } + return $method(join('&', $params)); + } + + /** + * 生成支付签名 + * @param array $option + * @param string $partnerKey + * @return string + */ + static public function getPaySign($option, $partnerKey) { + ksort($option); + $buff = ''; + foreach ($option as $k => $v) { + $buff .= "{$k}={$v}&"; + } + return strtoupper(md5("{$buff}key={$partnerKey}")); + } + + /** + * XML编码 + * @param mixed $data 数据 + * @param string $root 根节点名 + * @param string $item 数字索引的子节点名 + * @param string $id 数字索引子节点key转换的属性名 + * @return string + */ + static public function arr2xml($data, $root = 'xml', $item = 'item', $id = 'id') { + return "<{$root}>" . self::_data_to_xml($data, $item, $id) . ""; + } + + static private function _data_to_xml($data, $item = 'item', $id = 'id', $content = '') { + foreach ($data as $key => $val) { + is_numeric($key) && $key = "{$item} {$id}=\"{$key}\""; + $content .= "<{$key}>"; + if (is_array($val) || is_object($val)) { + $content .= self::_data_to_xml($val); + } elseif (is_numeric($val)) { + $content .= $val; + } else { + $content .= ''; + } + list($_key,) = explode(' ', $key . ' '); + $content .= ""; + } + return $content; + } + + + /** + * 将xml转为array + * @param string $xml + * @return array + */ + static public function xml2arr($xml) { + return json_decode(Tools::json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true); + } + + /** + * 生成安全JSON数据 + * @param array $array + * @return string + */ + static public function json_encode($array) { + return preg_replace_callback('/\\\\u([0-9a-f]{4})/i', create_function('$matches', 'return mb_convert_encoding(pack("H*", $matches[1]), "UTF-8", "UCS-2BE");'), json_encode($array)); + } + + /** + * 以get方式提交请求 + * @param $url + * @return bool|mixed + */ + static public function httpGet($url) { + $oCurl = curl_init(); + if (stripos($url, "https://") !== FALSE) { + curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, FALSE); + curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($oCurl, CURLOPT_SSLVERSION, 1); + } + curl_setopt($oCurl, CURLOPT_URL, $url); + curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, 1); + $sContent = curl_exec($oCurl); + $aStatus = curl_getinfo($oCurl); + curl_close($oCurl); + if (intval($aStatus["http_code"]) == 200) { + return $sContent; + } else { + return false; + } + } + + /** + * 以post方式提交请求 + * @param string $url + * @param array|string $data + * @return bool|mixed + */ + static public function httpPost($url, $data) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); + curl_setopt($ch, CURLOPT_HEADER, FALSE); + curl_setopt($ch, CURLOPT_POST, TRUE); + if (is_array($data)) { + foreach ($data as &$value) { + if (is_string($value) && stripos($value, '@') === 0 && class_exists('CURLFile', FALSE)) { + $value = new CURLFile(realpath(trim($value, '@'))); + } + } + } + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + $data = curl_exec($ch); + curl_close($ch); + if ($data) { + return $data; + } + return false; + } + + /** + * 使用证书,以post方式提交xml到对应的接口url + * @param string $url POST提交的内容 + * @param array $postdata 请求的地址 + * @param string $ssl_cer 证书Cer路径 | 证书内容 + * @param string $ssl_key 证书Key路径 | 证书内容 + * @param int $second 设置请求超时时间 + * @return bool|mixed + */ + static public function httpsPost($url, $postdata, $ssl_cer = null, $ssl_key = null, $second = 30) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_TIMEOUT, $second); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($ch, CURLOPT_HEADER, FALSE); + /* 要求结果为字符串且输出到屏幕上 */ + curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); + /* 设置证书 */ + if (!is_null($ssl_cer) && file_exists($ssl_cer) && is_file($ssl_cer)) { + curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM'); + curl_setopt($ch, CURLOPT_SSLCERT, $ssl_cer); + } + if (!is_null($ssl_key) && file_exists($ssl_key) && is_file($ssl_key)) { + curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM'); + curl_setopt($ch, CURLOPT_SSLKEY, $ssl_key); + } + curl_setopt($ch, CURLOPT_POST, true); + if (is_array($postdata)) { + foreach ($postdata as &$data) { + if (is_string($data) && stripos($data, '@') === 0 && class_exists('CURLFile', FALSE)) { + $data = new CURLFile(realpath(trim($data, '@'))); + } + } + } + curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata); + $result = curl_exec($ch); + curl_close($ch); + if ($result) { + return $result; + } else { + return false; + } + } + + /** + * 读取微信客户端IP + * @return null|string + */ + static public function getAddress() { + foreach (array('HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_X_CLIENT_IP', 'HTTP_X_CLUSTER_CLIENT_IP', 'REMOTE_ADDR') as $header) { + if (!isset($_SERVER[$header]) || ($spoof = $_SERVER[$header]) === NULL) { + continue; + } + sscanf($spoof, '%[^,]', $spoof); + if (!filter_var($spoof, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + $spoof = NULL; + } else { + return $spoof; + } + } + return '0.0.0.0'; + } + + /** + * 设置缓存,按需重载 + * @param string $cachename + * @param mixed $value + * @param int $expired + * @return bool + */ + static public function setCache($cachename, $value, $expired = 0) { + return Cache::set($cachename, $value, $expired); + } + + /** + * 获取缓存,按需重载 + * @param string $cachename + * @return mixed + */ + static public function getCache($cachename) { + return Cache::get($cachename); + } + + /** + * 清除缓存,按需重载 + * @param string $cachename + * @return bool + */ + static public function removeCache($cachename) { + return Cache::del($cachename); + } + + /** + * SDK日志处理方法 + * @param string $msg 日志行内容 + * @param string $type 日志级别 + */ + static public function log($msg, $type = 'MSG') { + Cache::put($type . ' - ' . $msg); + } + +} diff --git a/core/extend/Wechat/Loader.php b/core/extend/Wechat/Loader.php new file mode 100644 index 00000000..475193e0 --- /dev/null +++ b/core/extend/Wechat/Loader.php @@ -0,0 +1,111 @@ + + * @date 2016/10/26 10:21 + */ +spl_autoload_register(function ($class) { + if (0 === stripos($class, 'Wechat\\')) { + $filename = dirname(__DIR__) . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php'; + file_exists($filename) && require($filename); + } +}); + +/** + * 微信SDK加载器 + * @author Anyon + * @date 2016-08-21 11:06 + */ +class Loader { + + /** + * 事件注册函数 + * @var array + */ + static public $callback = array(); + + /** + * 配置参数 + * @var array + */ + static protected $config = array(); + + /** + * 对象缓存 + * @var array + */ + static protected $cache = array(); + + /** + * 动态注册SDK事件处理函数 + * @param string $event 事件名称(getAccessToken|getJsTicket) + * @param string $method 处理方法(可以是普通方法或者类中的方法) + * @param string|null $class 处理对象(可以直接使用的类实例) + */ + static public function register($event, $method, $class = NULL) { + if (!empty($class) && class_exists($class, FALSE) && method_exists($class, $method)) { + self::$callback[$event] = array($class, $method); + } else { + self::$callback[$event] = $method; + } + } + + /** + * 获取微信SDK接口对象(别名函数) + * @param string $type 接口类型(Card|Custom|Device|Extends|Media|Menu|Oauth|Pay|Receive|Script|User|Poi) + * @param array $config SDK配置(token,appid,appsecret,encodingaeskey,mch_id,partnerkey,ssl_cer,ssl_key,qrc_img) + * @return WechatCard|WechatCustom|WechatDevice|WechatExtends|WechatMedia|WechatMenu|WechatOauth|WechatPay|WechatPoi|WechatReceive|WechatScript|WechatService|WechatUser + */ + static public function & get_instance($type, $config = array()) { + return self::get($type, $config); + } + + /** + * 获取微信SDK接口对象 + * @param string $type 接口类型(Card|Custom|Device|Extends|Media|Menu|Oauth|Pay|Receive|Script|User|Poi) + * @param array $config SDK配置(token,appid,appsecret,encodingaeskey,mch_id,partnerkey,ssl_cer,ssl_key,qrc_img) + * @return WechatCard|WechatCustom|WechatDevice|WechatExtends|WechatMedia|WechatMenu|WechatOauth|WechatPay|WechatPoi|WechatReceive|WechatScript|WechatService|WechatUser + */ + static public function & get($type, $config = array()) { + $index = md5(strtolower($type) . md5(json_encode(self::$config))); + if (!isset(self::$cache[$index])) { + $basicName = 'Wechat' . ucfirst(strtolower($type)); + $className = "\\Wechat\\{$basicName}"; + // 注册类的无命名空间别名,兼容未带命名空间的老版本SDK + !class_exists($basicName, FALSE) && class_alias($className, $basicName); + self::$cache[$index] = new $className(self::config($config)); + } + return self::$cache[$index]; + } + + /** + * 设置配置参数 + * @param array $config + * @return array + */ + static public function config($config = array()) { + !empty($config) && self::$config = array_merge(self::$config, $config); + if (!empty(self::$config['cachepath'])) { + Cache::$cachepath = self::$config['cachepath']; + } + if (empty(self::$config['component_verify_ticket'])) { + self::$config['component_verify_ticket'] = Cache::get('component_verify_ticket'); + } + if (empty(self::$config['token']) && !empty(self::$config['component_token'])) { + self::$config['token'] = self::$config['component_token']; + } + if (empty(self::$config['appsecret']) && !empty(self::$config['component_appsecret'])) { + self::$config['appsecret'] = self::$config['component_appsecret']; + } + if (empty(self::$config['encodingaeskey']) && !empty(self::$config['component_encodingaeskey'])) { + self::$config['encodingaeskey'] = self::$config['component_encodingaeskey']; + } + return self::$config; + } + +} diff --git a/core/extend/Wechat/WechatCard.php b/core/extend/Wechat/WechatCard.php new file mode 100644 index 00000000..5fb258f9 --- /dev/null +++ b/core/extend/Wechat/WechatCard.php @@ -0,0 +1,776 @@ +access_token && !$this->getAccessToken()) { + return false; + } + $appid = empty($appid) ? $this->appid : $appid; + if ($jsapi_ticket) { + return $jsapi_ticket; + } + $authname = 'wechat_jsapi_ticket_wxcard_' . $appid; + if (($jsapi_ticket = Tools::getCache($authname))) { + return $jsapi_ticket; + } + $result = Tools::httpGet(self::API_URL_PREFIX . self::GET_TICKET_URL . "access_token={$this->access_token}" . '&type=wx_card'); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + $expire = $json['expires_in'] ? intval($json['expires_in']) - 100 : 3600; + Tools::setCache($authname, $json['ticket'], $expire); + return $json['ticket']; + } + return false; + } + + /** + * 生成选择卡卷JS签名包 + * @param string $cardid 卡券Id + * @param string $cardtype 卡券类型 + * @param string $shopid 门店Id + * @return array + */ + public function createChooseCardJsPackage($cardid = NULL, $cardtype = NULL, $shopid = NULL) { + $data = array(); + $data['api_ticket'] = $this->getJsCardTicket(); + $data['app_id'] = $this->appid; + $data['timestamp'] = time(); + $data['nonceStr'] = Tools::createNoncestr(); + !empty($cardid) && $data['cardId'] = $cardid; + !empty($cardtype) && $data['cardType'] = $cardtype; + !empty($shopid) && $data['shopId'] = $shopid; + $data['cardSign'] = $this->getTicketSignature($data); + $data['signType'] = 'SHA1'; + unset($data['api_ticket'], $data['app_id']); + return $data; + } + + /** + * 生成添加卡卷JS签名包 + * @param string|null $cardid 卡卷ID + * @param array $data 其它限定参数 + * @return array + */ + public function createAddCardJsPackage($cardid = NULL, $data = array()) { + + function _sign($cardid = NULL, $attr = array(), $self) { + unset($attr['outer_id']); + $attr['cardId'] = $cardid; + $attr['timestamp'] = time(); + $attr['api_ticket'] = $self->getJsCardTicket(); + $attr['nonce_str'] = Tools::createNoncestr(); + $attr['signature'] = $self->getTicketSignature($attr); + unset($attr['api_ticket']); + return $attr; + } + + $cardList = array(); + if (is_array($cardid)) { + foreach ($cardid as $id) { + $cardList[] = array('cardId' => $id, 'cardExt' => json_encode(_sign($id, $data, $this))); + } + } else { + $cardList[] = array('cardId' => $cardid, 'cardExt' => json_encode(_sign($cardid, $data, $this))); + } + return array('cardList' => $cardList); + } + + /** + * 获取微信卡券签名 + * @param array $arrdata 签名数组 + * @param string $method 签名方法 + * @return bool|string 签名值 + */ + public function getTicketSignature($arrdata, $method = "sha1") { + if (!function_exists($method)) { + return false; + } + $newArray = array(); + foreach ($arrdata as $value) { + array_push($newArray, (string)$value); + } + sort($newArray, SORT_STRING); + return $method(implode($newArray)); + } + + /** + * 创建卡券 + * @param array $data 卡券数据 + * @return bool|array 返回数组中card_id为卡券ID + */ + public function createCard($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_CREATE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 更改卡券信息 + * 调用该接口更新信息后会重新送审,卡券状态变更为待审核。已被用户领取的卡券会实时更新票面信息。 + * @param string $data + * @return bool + */ + public function updateCard($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_UPDATE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 删除卡券 + * 允许商户删除任意一类卡券。删除卡券后,该卡券对应已生成的领取用二维码、添加到卡包 JS API 均会失效。 + * 注意:删除卡券不能删除已被用户领取,保存在微信客户端中的卡券,已领取的卡券依旧有效。 + * @param string $card_id 卡券ID + * @return bool + */ + public function delCard($card_id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('card_id' => $card_id); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_DELETE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 获取粉丝下所有卡卷列表 + * @param $openid 粉丝openid + * @param string $card_id 卡卷ID(可不给) + * @return bool|array + */ + public function getCardList($openid, $card_id = '') { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('openid' => $openid); + !empty($card_id) && $data['card_id'] = $card_id; + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_USER_GET_LIST . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode']) || empty($json['card_list'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取图文消息群发卡券HTML + * @param string $card_id 卡卷ID + * @return bool|array + */ + public function getCardMpHtml($card_id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('card_id' => $card_id); + !empty($card_id) && $data['card_id'] = $card_id; + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_SEND_HTML . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode']) || empty($json['card_list'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 卡卷code核查 + * @param string $card_id 卡卷ID + * @param array $code_list 卡卷code列表(一维数组) + * @return bool|array + */ + public function checkCardCodeList($card_id, $code_list) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('card_id' => $card_id, 'code' => $code_list); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_CHECKCODE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode']) || empty($json['card_list'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 查询卡券详情 + * @param string $card_id 卡卷ID + * @return bool|array + */ + public function getCardInfo($card_id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('card_id' => $card_id); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_GET . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取颜色列表 + * 获得卡券的最新颜色列表,用于创建卡券 + * @return bool|array + */ + public function getCardColors() { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_BASE_URL_PREFIX . self::CARD_GETCOLORS . "access_token={$this->access_token}"); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 生成卡券二维码 + * 成功则直接返回ticket值,可以用 getQRUrl($ticket) 换取二维码url + * @param string $card_id 卡券ID 必须 + * @param string $code 指定卡券 code 码,只能被领一次。use_custom_code 字段为 true 的卡券必须填写,非自定义 code 不必填写。 + * @param string $openid 指定领取者的 openid,只有该用户能领取。bind_openid 字段为 true 的卡券必须填写,非自定义 openid 不必填写。 + * @param int $expire_seconds 指定二维码的有效时间,范围是 60 ~ 1800 秒。不填默认为永久有效。 + * @param bool $is_unique_code 指定下发二维码,生成的二维码随机分配一个 code,领取后不可再次扫描。填写 true 或 false。默认 false。 + * @param string $balance 红包余额,以分为单位。红包类型必填(LUCKY_MONEY),其他卡券类型不填。 + * @return bool|string + */ + public function createCardQrcode($card_id, $code = '', $openid = '', $expire_seconds = 0, $is_unique_code = false, $balance = '') { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $card = array('card_id' => $card_id); + !empty($code) && $card['code'] = $code; + !empty($openid) && $card['openid'] = $openid; + !empty($is_unique_code) && $card['is_unique_code'] = $is_unique_code; + !empty($balance) && $card['balance'] = $balance; + $data = array('action_name' => "QR_CARD"); + !empty($expire_seconds) && $data['expire_seconds'] = $expire_seconds; + $data['action_info'] = array('card' => $card); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_QRCODE_CREATE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 消耗 code + * 自定义 code(use_custom_code 为 true)的优惠券,在 code 被核销时,必须调用此接口。 + * @param string $code 要消耗的序列号 + * @param string $card_id 要消耗序列号所述的 card_id,创建卡券时use_custom_code 填写 true 时必填。 + * @return bool|array + * { + * "errcode":0, + * "errmsg":"ok", + * "card":{"card_id":"pFS7Fjg8kV1IdDz01r4SQwMkuCKc"}, + * "openid":"oFS7Fjl0WsZ9AMZqrI80nbIq8xrA" + * } + */ + public function consumeCardCode($code, $card_id = '') { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('code' => $code); + !empty($card_id) && $data['card_id'] = $card_id; + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_CODE_CONSUME . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * code 解码 + * @param string $encrypt_code 通过 choose_card_info 获取的加密字符串 + * @return bool|array + * { + * "errcode":0, + * "errmsg":"ok", + * "code":"751234212312" + * } + */ + public function decryptCardCode($encrypt_code) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('encrypt_code' => $encrypt_code,); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_CODE_DECRYPT . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 查询 code 的有效性(非自定义 code) + * @param string $code + * @return bool|array + * { + * "errcode":0, + * "errmsg":"ok", + * "openid":"oFS7Fjl0WsZ9AMZqrI80nbIq8xrA", //用户 openid + * "card":{ + * "card_id":"pFS7Fjg8kV1IdDz01r4SQwMkuCKc", + * "begin_time": 1404205036, //起始使用时间 + * "end_time": 1404205036, //结束时间 + * } + * } + */ + public function checkCardCode($code) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('code' => $code); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_CODE_GET . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 批量查询卡列表 + * @param int $offset 开始拉取的偏移,默认为0从头开始 + * @param int $count 需要查询的卡片的数量(数量最大50,默认50) + * @return bool|array + * { + * "errcode":0, + * "errmsg":"ok", + * "card_id_list":["ph_gmt7cUVrlRk8swPwx7aDyF-pg"], //卡 id 列表 + * "total_num":1 //该商户名下 card_id 总数 + * } + */ + public function getCardIdList($offset = 0, $count = 50) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $count > 50 && $count = 50; + $data = array('offset' => $offset, 'count' => $count); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_BATCHGET . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 更改 code + * 为确保转赠后的安全性,微信允许自定义code的商户对已下发的code进行更改。 + * 注:为避免用户疑惑,建议仅在发生转赠行为后(发生转赠后,微信会通过事件推送的方式告知商户被转赠的卡券code)对用户的code进行更改。 + * @param string $code 卡券的 code 编码 + * @param string $card_id 卡券 ID + * @param string $new_code 新的卡券 code 编码 + * @return bool + */ + public function updateCardCode($code, $card_id, $new_code) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('code' => $code, 'card_id' => $card_id, 'new_code' => $new_code); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_CODE_UPDATE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 设置卡券失效 + * 设置卡券失效的操作不可逆 + * @param string $code 需要设置为失效的 code + * @param string $card_id 自定义 code 的卡券必填。非自定义 code 的卡券不填。 + * @return bool + */ + public function unavailableCardCode($code, $card_id = '') { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('code' => $code); + !empty($card_id) && $data['card_id'] = $card_id; + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_CODE_UNAVAILABLE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 库存修改 + * @param string $data + * @return bool + */ + public function modifyCardStock($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_MODIFY_STOCK . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 更新门票 + * @param string $data + * @return bool + */ + public function updateMeetingCard($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_MEETINGCARD_UPDATEUSER . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 激活/绑定会员卡 + * @param string $data 具体结构请参看卡券开发文档(6.1.1 激活/绑定会员卡)章节 + * @return bool + */ + public function activateMemberCard($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_MEMBERCARD_ACTIVATE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 会员卡交易 + * 会员卡交易后每次积分及余额变更需通过接口通知微信,便于后续消息通知及其他扩展功能。 + * @param string $data 具体结构请参看卡券开发文档(6.1.2 会员卡交易)章节 + * @return bool|array + */ + public function updateMemberCard($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_MEMBERCARD_UPDATEUSER . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 设置卡券测试白名单 + * @param array $openid 测试的 openid 列表 + * @param array $user 测试的微信号列表 + * @return bool + */ + public function setCardTestWhiteList($openid = array(), $user = array()) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array(); + count($openid) > 0 && $data['openid'] = $openid; + count($user) > 0 && $data['username'] = $user; + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_TESTWHILELIST_SET . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 更新红包金额 + * @param string $code 红包的序列号 + * @param int $balance 红包余额 + * @param string $card_id 自定义 code 的卡券必填。非自定义 code 可不填。 + * @return bool|array + */ + public function updateLuckyMoney($code, $balance, $card_id = '') { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('code' => $code, 'balance' => $balance); + !empty($card_id) && $data['card_id'] = $card_id; + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_LUCKYMONEY_UPDATE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 设置自助核销接口 + * @param string $card_id 卡券ID + * @param bool $is_openid 是否开启自助核销功能,填true/false,默认为false + * @param bool $need_verify_cod 用户核销时是否需要输入验证码,填true/false,默认为false + * @param bool $need_remark_amount 用户核销时是否需要备注核销金额,填true/false,默认为false + * @return bool|array + */ + public function setSelfconsumecell($card_id, $is_openid = false, $need_verify_cod = false, $need_remark_amount = false) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array( + 'card_id' => $card_id, + 'is_open' => $is_openid, + 'need_verify_cod' => $need_verify_cod, + 'need_remark_amount' => $need_remark_amount, + ); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_SET_SELFCONSUMECELL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 设置买单接口 + * @param string $card_id + * @param bool $is_openid + * @return bool|mixed + */ + public function setPaycell($card_id, $is_openid = true) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array( + 'card_id' => $card_id, + 'is_open' => $is_openid, + ); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_PAYCELL_SET . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 设置开卡字段信息接口 + * @param array $data + * @return bool|array + */ + public function setMembercardActivateuserform($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CARD_MEMBERCARD_ACTIVATEUSERFORM_SET . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + +} diff --git a/core/extend/Wechat/WechatCustom.php b/core/extend/Wechat/WechatCustom.php new file mode 100644 index 00000000..00482718 --- /dev/null +++ b/core/extend/Wechat/WechatCustom.php @@ -0,0 +1,338 @@ +access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::CUSTOM_SERVICE_GET_RECORD . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return false; + } + return $json; + } + return false; + } + + /** + * 获取多客服客服基本信息 + * + * @return bool|array + */ + public function getCustomServiceKFlist() { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_URL_PREFIX . self::CUSTOM_SERVICE_GET_KFLIST . "access_token={$this->access_token}"); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取多客服在线客服接待信息 + * + * @return bool|array + */ + public function getCustomServiceOnlineKFlist() { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_URL_PREFIX . self::CUSTOM_SERVICE_GET_ONLINEKFLIST . "access_token={$this->access_token}"); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 创建指定多客服会话 + * @tutorial 当用户已被其他客服接待或指定客服不在线则会失败 + * @param string $openid //用户openid + * @param string $kf_account //客服账号 + * @param string $text //附加信息,文本会展示在客服人员的多客服客户端,可为空 + * @return bool|array + */ + public function createKFSession($openid, $kf_account, $text = '') { + $data = array("openid" => $openid, "kf_account" => $kf_account); + if ($text) { + $data["text"] = $text; + } + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CUSTOM_SESSION_CREATE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 关闭指定多客服会话 + * @tutorial 当用户被其他客服接待时则会失败 + * @param string $openid //用户openid + * @param string $kf_account //客服账号 + * @param string $text //附加信息,文本会展示在客服人员的多客服客户端,可为空 + * @return bool | array //成功返回json数组 + * { + * "errcode": 0, + * "errmsg": "ok", + * } + */ + public function closeKFSession($openid, $kf_account, $text = '') { + $data = array("openid" => $openid, "kf_account" => $kf_account); + if ($text) { + $data["text"] = $text; + } + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CUSTOM_SESSION_CLOSE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取用户会话状态 + * @param string $openid //用户openid + * @return bool | array //成功返回json数组 + * { + * "errcode" : 0, + * "errmsg" : "ok", + * "kf_account" : "test1@test", //正在接待的客服 + * "createtime": 123456789, //会话接入时间 + * } + */ + public function getKFSession($openid) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_BASE_URL_PREFIX . self::CUSTOM_SESSION_GET . "access_token={$this->access_token}" . '&openid=' . $openid); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取指定客服的会话列表 + * @param string $kf_account //用户openid + * @return bool | array //成功返回json数组 + * array( + * 'sessionlist' => array ( + * array ( + * 'openid'=>'OPENID', //客户 openid + * 'createtime'=>123456789, //会话创建时间,UNIX 时间戳 + * ), + * array ( + * 'openid'=>'OPENID', //客户 openid + * 'createtime'=>123456789, //会话创建时间,UNIX 时间戳 + * ), + * ) + * ) + */ + public function getKFSessionlist($kf_account) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_BASE_URL_PREFIX . self::CUSTOM_SESSION_GET_LIST . "access_token={$this->access_token}" . '&kf_account=' . $kf_account); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取未接入会话列表 + * @return bool|array + */ + public function getKFSessionWait() { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_BASE_URL_PREFIX . self::CUSTOM_SESSION_GET_WAIT . "access_token={$this->access_token}"); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 添加客服账号 + * + * @param string $account 完整客服账号(账号前缀@公众号微信号,账号前缀最多10个字符) + * @param string $nickname 客服昵称,最长6个汉字或12个英文字符 + * @param string $password 客服账号明文登录密码,会自动加密 + * @return bool|array + */ + public function addKFAccount($account, $nickname, $password) { + $data = array("kf_account" => $account, "nickname" => $nickname, "password" => md5($password)); + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CS_KF_ACCOUNT_ADD_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 修改客服账号信息 + * + * @param string $account //完整客服账号,格式为:账号前缀@公众号微信号,账号前缀最多10个字符,必须是英文或者数字字符 + * @param string $nickname //客服昵称,最长6个汉字或12个英文字符 + * @param string $password //客服账号明文登录密码,会自动加密 + * @return bool|array + * 成功返回结果 + * { + * "errcode": 0, + * "errmsg": "ok", + * } + */ + public function updateKFAccount($account, $nickname, $password) { + $data = array("kf_account" => $account, "nickname" => $nickname, "password" => md5($password)); + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CS_KF_ACCOUNT_UPDATE_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 删除客服账号 + * @param string $account 完整客服账号(账号前缀@公众号微信号,账号前缀最多10个字符) + * @return bool|array + */ + public function deleteKFAccount($account) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_BASE_URL_PREFIX . self::CS_KF_ACCOUNT_DEL_URL . "access_token={$this->access_token}" . '&kf_account=' . $account); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 上传客服头像 + * @param string $account 完整客服账号(账号前缀@公众号微信号,账号前缀最多10个字符) + * @param string $imgfile 头像文件完整路径,如:'D:\user.jpg'。头像文件必须JPG格式,像素建议640*640 + * @return bool|array + */ + public function setKFHeadImg($account, $imgfile) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::CS_KF_ACCOUNT_UPLOAD_HEADIMG_URL . "access_token={$this->access_token}" . '&kf_account=' . $account, array('media' => '@' . $imgfile), true); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + +} diff --git a/core/extend/Wechat/WechatDevice.php b/core/extend/Wechat/WechatDevice.php new file mode 100644 index 00000000..8126f94f --- /dev/null +++ b/core/extend/Wechat/WechatDevice.php @@ -0,0 +1,441 @@ + + * @date 2016-08-22 10:35 + */ +class WechatDevice extends Common { + + const SHAKEAROUND_DEVICE_APPLYID = '/shakearound/device/applyid?'; //申请设备ID + const SHAKEAROUND_DEVICE_APPLYSTATUS = '/shakearound/device/applystatus?'; //查询设备ID申请审核状态 + const SHAKEAROUND_DEVICE_UPDATE = '/shakearound/device/update?'; //编辑设备信息 + const SHAKEAROUND_DEVICE_SEARCH = '/shakearound/device/search?'; //查询设备列表 + const SHAKEAROUND_DEVICE_BINDLOCATION = '/shakearound/device/bindlocation?'; //配置设备与门店ID的关系 + const SHAKEAROUND_DEVICE_BINDPAGE = '/shakearound/device/bindpage?'; //配置设备与页面的绑定关系 + const SHAKEAROUND_MATERIAL_ADD = '/shakearound/material/add?'; //上传摇一摇图片素材 + const SHAKEAROUND_PAGE_ADD = '/shakearound/page/add?'; //增加页面 + const SHAKEAROUND_PAGE_UPDATE = '/shakearound/page/update?'; //编辑页面 + const SHAKEAROUND_PAGE_SEARCH = '/shakearound/page/search?'; //查询页面列表 + const SHAKEAROUND_PAGE_DELETE = '/shakearound/page/delete?'; //删除页面 + const SHAKEAROUND_USER_GETSHAKEINFO = '/shakearound/user/getshakeinfo?'; //获取摇周边的设备及用户信息 + const SHAKEAROUND_STATISTICS_DEVICE = '/shakearound/statistics/device?'; //以设备为维度的数据统计接口 + const SHAKEAROUND_STATISTICS_PAGE = '/shakearound/statistics/page?'; //以页面为维度的数据统计接口 + + + /** + * 申请设备ID + * @param array $data + * @return bool|array + */ + public function applyShakeAroundDevice($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_DEVICE_APPLYID . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 查询设备ID申请审核状态 + * @param int $apply_id + * @return bool|array + */ + public function applyStatusShakeAroundDevice($apply_id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array("apply_id" => $apply_id); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_DEVICE_APPLYSTATUS . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 编辑设备信息 + * @param array $data + * @return bool + */ + public function updateShakeAroundDevice($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_DEVICE_UPDATE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + + /** + * 查询设备列表 + * @param $data + * @return bool|array + */ + public function searchShakeAroundDevice($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_DEVICE_SEARCH . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 配置设备与门店的关联关系 + * @param string $device_id 设备编号,若填了UUID、major、minor,则可不填设备编号,若二者都填,则以设备编号为优先 + * @param int $poi_id 待关联的门店ID + * @param string $uuid UUID、major、minor,三个信息需填写完整,若填了设备编号,则可不填此信息 + * @param int $major + * @param int $minor + * @return bool|array + */ + public function bindLocationShakeAroundDevice($device_id, $poi_id, $uuid = '', $major = 0, $minor = 0) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + if (!$device_id) { + if (!$uuid || !$major || !$minor) { + return false; + } + $device_identifier = array('uuid' => $uuid, 'major' => $major, 'minor' => $minor); + } else { + $device_identifier = array( + 'device_id' => $device_id + ); + } + $data = array('device_identifier' => $device_identifier, 'poi_id' => $poi_id); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_DEVICE_BINDLOCATION . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; //这个可以更改为返回true + } + return false; + } + + /** + * 配置设备与其他公众账号门店的关联关系 + * @param type $device_identifier 设备信息 + * @param type $poi_id 待关联的门店ID + * @param type $poi_appid 目标微信appid + * @return boolean + */ + public function bindLocationOtherShakeAroundDevice($device_identifier, $poi_id, $poi_appid) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('device_identifier' => $device_identifier, 'poi_id' => $poi_id, "type" => 2, "poi_appid" => $poi_appid); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_DEVICE_BINDLOCATION . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; //这个可以更改为返回true + } + return false; + } + + /** + * 配置设备与页面的关联关系 + * @param string $device_id 设备编号,若填了UUID、major、minor,则可不填设备编号,若二者都填,则以设备编号为优先 + * @param array $page_ids 待关联的页面列表 + * @param int $bind 关联操作标志位, 0 为解除关联关系,1 为建立关联关系 + * @param int $append 新增操作标志位, 0 为覆盖,1 为新增 + * @param string $uuid UUID、major、minor,三个信息需填写完整,若填了设备编号,则可不填此信息 + * @param int $major + * @param int $minor + * @return bool|array + */ + public function bindPageShakeAroundDevice($device_id, $page_ids = array(), $bind = 1, $append = 1, $uuid = '', $major = 0, $minor = 0) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + if (!$device_id) { + if (!$uuid || !$major || !$minor) { + return false; + } + $device_identifier = array('uuid' => $uuid, 'major' => $major, 'minor' => $minor); + } else { + $device_identifier = array('device_id' => $device_id); + } + $data = array('device_identifier' => $device_identifier, 'page_ids' => $page_ids, 'bind' => $bind, 'append' => $append); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_DEVICE_BINDPAGE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + + /** + * 上传在摇一摇页面展示的图片素材 + * @param array $data {"media":'@Path\filename.jpg'} + * @return bool|array + */ + public function uploadShakeAroundMedia($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_MATERIAL_ADD . "access_token={$this->access_token}", $data, true); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + + /** + * 增加摇一摇出来的页面信息 + * @param string $title 在摇一摇页面展示的主标题,不超过6 个字 + * @param string $description 在摇一摇页面展示的副标题,不超过7 个字 + * @param string $icon_url 在摇一摇页面展示的图片, 格式限定为:jpg,jpeg,png,gif; 建议120*120 , 限制不超过200*200 + * @param string $page_url 跳转链接 + * @param string $comment 页面的备注信息,不超过15 个字,可不填 + * @return bool|array + */ + public function addShakeAroundPage($title, $description, $icon_url, $page_url, $comment = '') { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array("title" => $title, "description" => $description, "icon_url" => $icon_url, "page_url" => $page_url, "comment" => $comment); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_PAGE_ADD . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + + /** + * 编辑摇一摇出来的页面信息 + * @param int $page_id + * @param string $title 在摇一摇页面展示的主标题,不超过6 个字 + * @param string $description 在摇一摇页面展示的副标题,不超过7 个字 + * @param string $icon_url 在摇一摇页面展示的图片, 格式限定为:jpg,jpeg,png,gif; 建议120*120 , 限制不超过200*200 + * @param string $page_url 跳转链接 + * @param string $comment 页面的备注信息,不超过15 个字,可不填 + * @return bool|array + */ + public function updateShakeAroundPage($page_id, $title, $description, $icon_url, $page_url, $comment = '') { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array("page_id" => $page_id, "title" => $title, "description" => $description, "icon_url" => $icon_url, "page_url" => $page_url, "comment" => $comment); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_PAGE_UPDATE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + + /** + * 查询已有的页面 + * @param array $page_ids + * @param int $begin + * @param int $count + * @return bool|mixed + */ + public function searchShakeAroundPage($page_ids = array(), $begin = 0, $count = 1) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + if (!empty($page_ids)) { + $data = array('page_ids' => $page_ids); + } else { + $data = array('begin' => $begin, 'count' => $count); + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_PAGE_SEARCH . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + + /** + * 删除已有的页面 + * @param array $page_ids + * @return bool|array + */ + public function deleteShakeAroundPage($page_ids = array()) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('page_ids' => $page_ids); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_PAGE_DELETE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + + /** + * 获取设备信息 + * @param string $ticket 摇周边业务的ticket(可在摇到的URL中得到,ticket生效时间为30 分钟) + * @return bool|array + */ + public function getShakeInfoShakeAroundUser($ticket) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('ticket' => $ticket); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_USER_GETSHAKEINFO . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + + /** + * 以设备为维度的数据统计接口 + * @param int $device_id 设备编号,若填了UUID、major、minor,即可不填设备编号,二者选其一 + * @param int $begin_date 起始日期时间戳,最长时间跨度为30 天 + * @param int $end_date 结束日期时间戳,最长时间跨度为30 天 + * @param string $uuid UUID、major、minor,三个信息需填写完成,若填了设备编辑,即可不填此信息,二者选其一 + * @param int $major + * @param int $minor + * @return bool|array + */ + public function deviceShakeAroundStatistics($device_id, $begin_date, $end_date, $uuid = '', $major = 0, $minor = 0) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + if (!$device_id) { + if (!$uuid || !$major || !$minor) { + return false; + } + $device_identifier = array('uuid' => $uuid, 'major' => $major, 'minor' => $minor); + } else { + $device_identifier = array('device_id' => $device_id); + } + $data = array('device_identifier' => $device_identifier, 'begin_date' => $begin_date, 'end_date' => $end_date); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_STATISTICS_DEVICE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + + /** + * 以页面为维度的数据统计接口 + * @param int $page_id 指定页面的ID + * @param int $begin_date 起始日期时间戳,最长时间跨度为30 天 + * @param int $end_date 结束日期时间戳,最长时间跨度为30 天 + * @return bool|array + */ + public function pageShakeAroundStatistics($page_id, $begin_date, $end_date) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('page_id' => $page_id, 'begin_date' => $begin_date, 'end_date' => $end_date); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SHAKEAROUND_STATISTICS_DEVICE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + +} diff --git a/core/extend/Wechat/WechatExtends.php b/core/extend/Wechat/WechatExtends.php new file mode 100644 index 00000000..6a8e6ca8 --- /dev/null +++ b/core/extend/Wechat/WechatExtends.php @@ -0,0 +1,197 @@ + + * @date 2016-08-22 10:32 + */ +class WechatExtends extends Common { + + const QR_LIMIT_SCENE = 1; + + /** 语义理解 */ + const SEMANTIC_API_URL = '/semantic/semproxy/search?'; + const QRCODE_IMG_URL = 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket='; + const QRCODE_CREATE_URL = '/qrcode/create?'; + const SHORT_URL = '/shorturl?'; + const QR_SCENE = 0; + + /** 数据分析接口 */ + static $DATACUBE_URL_ARR = array(//用户分析 + 'user' => array( + 'summary' => '/datacube/getusersummary?', //获取用户增减数据(getusersummary) + 'cumulate' => '/datacube/getusercumulate?', //获取累计用户数据(getusercumulate) + ), + 'article' => array(//图文分析 + 'summary' => '/datacube/getarticlesummary?', //获取图文群发每日数据(getarticlesummary) + 'total' => '/datacube/getarticletotal?', //获取图文群发总数据(getarticletotal) + 'read' => '/datacube/getuserread?', //获取图文统计数据(getuserread) + 'readhour' => '/datacube/getuserreadhour?', //获取图文统计分时数据(getuserreadhour) + 'share' => '/datacube/getusershare?', //获取图文分享转发数据(getusershare) + 'sharehour' => '/datacube/getusersharehour?', //获取图文分享转发分时数据(getusersharehour) + ), + 'upstreammsg' => array(//消息分析 + 'summary' => '/datacube/getupstreammsg?', //获取消息发送概况数据(getupstreammsg) + 'hour' => '/datacube/getupstreammsghour?', //获取消息分送分时数据(getupstreammsghour) + 'week' => '/datacube/getupstreammsgweek?', //获取消息发送周数据(getupstreammsgweek) + 'month' => '/datacube/getupstreammsgmonth?', //获取消息发送月数据(getupstreammsgmonth) + 'dist' => '/datacube/getupstreammsgdist?', //获取消息发送分布数据(getupstreammsgdist) + 'distweek' => '/datacube/getupstreammsgdistweek?', //获取消息发送分布周数据(getupstreammsgdistweek) + 'distmonth' => '/datacube/getupstreammsgdistmonth?', //获取消息发送分布月数据(getupstreammsgdistmonth) + ), + 'interface' => array(//接口分析 + 'summary' => '/datacube/getinterfacesummary?', //获取接口分析数据(getinterfacesummary) + 'summaryhour' => '/datacube/getinterfacesummaryhour?', //获取接口分析分时数据(getinterfacesummaryhour) + ) + ); + + /** + * 获取二维码图片 + * @param string $ticket 传入由getQRCode方法生成的ticket参数 + * @return string url 返回http地址 + */ + public function getQRUrl($ticket) { + return self::QRCODE_IMG_URL . urlencode($ticket); + } + + /** + * 长链接转短链接接口 + * @param string $long_url 传入要转换的长url + * @return bool|string url 成功则返回转换后的短url + */ + public function getShortUrl($long_url) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array( + 'action' => 'long2short', + 'long_url' => $long_url + ); + $result = Tools::httpPost(self::API_URL_PREFIX . self::SHORT_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json['short_url']; + } + return false; + } + + /** + * 创建二维码ticket + * @param int|string $scene_id 自定义追踪id,临时二维码只能用数值型 + * @param int $type 0:临时二维码;1:永久二维码(此时expire参数无效);2:永久二维码(此时expire参数无效) + * @param int $expire 临时二维码有效期,最大为2592000秒(30天) + * @return bool|array ('ticket'=>'qrcode字串','expire_seconds'=>2592000,'url'=>'二维码图片解析后的地址') + */ + public function getQRCode($scene_id, $type = 0, $expire = 2592000) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $type = ($type && is_string($scene_id)) ? 2 : $type; + $data = array( + 'action_name' => $type ? ($type == 2 ? "QR_LIMIT_STR_SCENE" : "QR_LIMIT_SCENE") : "QR_SCENE", + 'expire_seconds' => $expire, + 'action_info' => array('scene' => ($type == 2 ? array('scene_str' => $scene_id) : array('scene_id' => $scene_id))) + ); + if ($type == 1) { + unset($data['expire_seconds']); + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::QRCODE_CREATE_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 语义理解接口 + * @param string $uid 用户唯一id(非开发者id),用户区分公众号下的不同用户(建议填入用户openid) + * @param string $query 输入文本串 + * @param string $category 需要使用的服务类型,多个用“,”隔开,不能为空 + * @param float $latitude 纬度坐标,与经度同时传入;与城市二选一传入 + * @param float $longitude 经度坐标,与纬度同时传入;与城市二选一传入 + * @param string $city 城市名称,与经纬度二选一传入 + * @param string $region 区域名称,在城市存在的情况下可省略;与经纬度二选一传入 + * @return bool|array + */ + public function querySemantic($uid, $query, $category, $latitude = 0.00, $longitude = 0.00, $city = "", $region = "") { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array( + 'query' => $query, + 'category' => $category, + 'appid' => $this->appid, + 'uid' => '' + ); + //地理坐标或城市名称二选一 + if ($latitude) { + $data['latitude'] = $latitude; + $data['longitude'] = $longitude; + } elseif ($city) { + $data['city'] = $city; + } elseif ($region) { + $data['region'] = $region; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::SEMANTIC_API_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取统计数据 + * @param string $type 数据分类(user|article|upstreammsg|interface)分别为(用户分析|图文分析|消息分析|接口分析) + * @param string $subtype 数据子分类,参考 DATACUBE_URL_ARR 常量定义部分 或者README.md说明文档 + * @param string $begin_date 开始时间 + * @param string $end_date 结束时间 + * @return bool|array 成功返回查询结果数组,其定义请看官方文档 + */ + public function getDatacube($type, $subtype, $begin_date, $end_date = '') { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + if (!isset(self::$DATACUBE_URL_ARR[$type]) || !isset(self::$DATACUBE_URL_ARR[$type][$subtype])) { + return false; + } + $data = array( + 'begin_date' => $begin_date, + 'end_date' => $end_date ? $end_date : $begin_date + ); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::$DATACUBE_URL_ARR[$type][$subtype] . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return isset($json['list']) ? $json['list'] : $json; + } + return false; + } + +} diff --git a/core/extend/Wechat/WechatHardware.php b/core/extend/Wechat/WechatHardware.php new file mode 100644 index 00000000..9ac4339c --- /dev/null +++ b/core/extend/Wechat/WechatHardware.php @@ -0,0 +1,142 @@ +access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::DEVICE_COMPEL_UNBINDHTTPS . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + + public function transmsg($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::DEVICE_TRANSMSG . "access_token={$this->access_token}", Tools::json_encode($data)); + //dump($result); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + public function getQrcode($product_id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_BASE_URL_PREFIX . self::DEVICE_GETQRCODE . "access_token={$this->access_token}&product_id=$product_id"); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 设备授权 + * @param $data + * @return bool|mixed + */ + public function deviceAuthorize($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::DEVICE_AUTHORIZE_DEVICE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取设备二维码 + * @param $data + * @return bool|mixed + */ + public function getDeviceQrcode($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::DEVICE_CREATE_QRCODE . "access_token={$this->access_token}", Tools::json_encode($data)); + //dump($result); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取设备状态 + * @param $device_id + * @return bool|mixed + */ + public function getDeviceStat($device_id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_BASE_URL_PREFIX . self::DEVICE_GET_STAT . "access_token={$this->access_token}&device_id=$device_id"); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + +} \ No newline at end of file diff --git a/core/extend/Wechat/WechatMedia.php b/core/extend/Wechat/WechatMedia.php new file mode 100644 index 00000000..3fff1663 --- /dev/null +++ b/core/extend/Wechat/WechatMedia.php @@ -0,0 +1,417 @@ + + * @date 2016/10/26 14:47 + */ +class WechatMedia extends Common { + + const UPLOAD_MEDIA_URL = 'http://file.api.weixin.qq.com/cgi-bin'; + const MEDIA_UPLOAD_URL = '/media/upload?'; + const MEDIA_UPLOADIMG_URL = '/media/uploadimg?'; //图片上传接口 + const MEDIA_GET_URL = '/media/get?'; + const MEDIA_VIDEO_UPLOAD = '/media/uploadvideo?'; + const MEDIA_FOREVER_UPLOAD_URL = '/material/add_material?'; + const MEDIA_FOREVER_NEWS_UPLOAD_URL = '/material/add_news?'; + const MEDIA_FOREVER_NEWS_UPDATE_URL = '/material/update_news?'; + const MEDIA_FOREVER_GET_URL = '/material/get_material?'; + const MEDIA_FOREVER_DEL_URL = '/material/del_material?'; + const MEDIA_FOREVER_COUNT_URL = '/material/get_materialcount?'; + const MEDIA_FOREVER_BATCHGET_URL = '/material/batchget_material?'; + const MEDIA_UPLOADNEWS_URL = '/media/uploadnews?'; + + /** + * 上传临时素材,有效期为3天(认证后的订阅号可用) + * 注意:上传大文件时可能需要先调用 set_time_limit(0) 避免超时 + * 注意:数组的键值任意,但文件名前必须加@,使用单引号以避免本地路径斜杠被转义 + * 注意:临时素材的media_id是可复用的! + * @param array $data {"media":'@Path\filename.jpg'} + * @param string $type 类型:图片:image 语音:voice 视频:video 缩略图:thumb + * @return bool|array + */ + public function uploadMedia($data, $type) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + //原先的上传多媒体文件接口使用 self::UPLOAD_MEDIA_URL 前缀 + $result = Tools::httpPost(self::API_URL_PREFIX . self::MEDIA_UPLOAD_URL . "access_token={$this->access_token}" . '&type=' . $type, $data, true); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取临时素材(认证后的订阅号可用) + * @param string $media_id 媒体文件id + * @param bool $is_video 是否为视频文件,默认为否 + * @return bool|array + */ + public function getMedia($media_id, $is_video = false) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + //原先的上传多媒体文件接口使用 self::UPLOAD_MEDIA_URL 前缀 + //如果要获取的素材是视频文件时,不能使用https协议,必须更换成http协议 + $url_prefix = $is_video ? str_replace('https', 'http', self::API_URL_PREFIX) : self::API_URL_PREFIX; + $result = Tools::httpGet($url_prefix . self::MEDIA_GET_URL . "access_token={$this->access_token}" . '&media_id=' . $media_id); + if ($result) { + if (is_string($result)) { + $json = json_decode($result, true); + if (isset($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + } + return $result; + } + return false; + } + + /** + * 获取临时素材(认证后的订阅号可用) 包含返回的http头信息 + * @param string $media_id 媒体文件id + * @param bool $is_video 是否为视频文件,默认为否 + * @return bool|array + */ + public function getMediaWithHttpInfo($media_id, $is_video = false) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + //原先的上传多媒体文件接口使用 self::UPLOAD_MEDIA_URL 前缀 + //如果要获取的素材是视频文件时,不能使用https协议,必须更换成http协议 + $url_prefix = $is_video ? str_replace('https', 'http', self::API_URL_PREFIX) : self::API_URL_PREFIX; + $url = $url_prefix . self::MEDIA_GET_URL . "access_token={$this->access_token}" . '&media_id=' . $media_id; + $oCurl = curl_init(); + if (stripos($url, "https://") !== FALSE) { + curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, FALSE); + curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($oCurl, CURLOPT_SSLVERSION, 1); + } + curl_setopt($oCurl, CURLOPT_URL, $url); + curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, 1); + $sContent = curl_exec($oCurl); + $aStatus = curl_getinfo($oCurl); + + $result = []; + + if (intval($aStatus["http_code"]) !== 200) { + return false; + } + + if ($sContent) { + if (is_string($sContent)) { + $json = json_decode($sContent, true); + if (isset($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + } + $result['content'] = $sContent; + $result['info'] = $aStatus; + return $result; + } + return false; + } + + /** + * 上传图片,本接口所上传的图片不占用公众号的素材库中图片数量的5000个的限制。图片仅支持jpg/png格式,大小必须在1MB以下。 (认证后的订阅号可用) + * 注意:上传大文件时可能需要先调用 set_time_limit(0) 避免超时 + * 注意:数组的键值任意,但文件名前必须加@,使用单引号以避免本地路径斜杠被转义 + * @param array $data {"media":'@Path\filename.jpg'} + * @return bool|array + */ + public function uploadImg($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + /* 原先的上传多媒体文件接口使用 self::UPLOAD_MEDIA_URL 前缀 */ + $result = Tools::httpPost(self::API_URL_PREFIX . self::MEDIA_UPLOADIMG_URL . "access_token={$this->access_token}", $data, true); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 上传永久素材(认证后的订阅号可用) + * 新增的永久素材也可以在公众平台官网素材管理模块中看到 + * 注意:上传大文件时可能需要先调用 set_time_limit(0) 避免超时 + * 注意:数组的键值任意,但文件名前必须加@,使用单引号以避免本地路径斜杠被转义 + * @param array $data {"media":'@Path\filename.jpg'} + * @param string $type 类型:图片:image 语音:voice 视频:video 缩略图:thumb + * @param bool $is_video 是否为视频文件,默认为否 + * @param array $video_info 视频信息数组,非视频素材不需要提供 array('title'=>'视频标题','introduction'=>'描述') + * @return bool|array + */ + public function uploadForeverMedia($data, $type, $is_video = false, $video_info = array()) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + if ($is_video) { + $data['description'] = Tools::json_encode($video_info); + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::MEDIA_FOREVER_UPLOAD_URL . "access_token={$this->access_token}" . '&type=' . $type, $data, true); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 上传永久图文素材(认证后的订阅号可用) + * 新增的永久素材也可以在公众平台官网素材管理模块中看到 + * @param array $data 消息结构{"articles":[{...}]} + * @return bool|array + */ + public function uploadForeverArticles($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::MEDIA_FOREVER_NEWS_UPLOAD_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 修改永久图文素材(认证后的订阅号可用) + * 永久素材也可以在公众平台官网素材管理模块中看到 + * @param string $media_id 图文素材id + * @param array $data 消息结构{"articles":[{...}]} + * @param int $index 更新的文章在图文素材的位置,第一篇为0,仅多图文使用 + * @return bool|array + */ + public function updateForeverArticles($media_id, $data, $index = 0) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + if (!isset($data['media_id'])) { + $data['media_id'] = $media_id; + } + if (!isset($data['index'])) { + $data['index'] = $index; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::MEDIA_FOREVER_NEWS_UPDATE_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取永久素材(认证后的订阅号可用) + * 返回图文消息数组或二进制数据,失败返回false + * @param string $media_id 媒体文件id + * @param bool $is_video 是否为视频文件,默认为否 + * @return bool|array|raw data + */ + public function getForeverMedia($media_id, $is_video = false) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('media_id' => $media_id); + //#TODO 暂不确定此接口是否需要让视频文件走http协议 + //如果要获取的素材是视频文件时,不能使用https协议,必须更换成http协议 + //$url_prefix = $is_video?str_replace('https','http',self::API_URL_PREFIX):self::API_URL_PREFIX; + $result = Tools::httpPost(self::API_URL_PREFIX . self::MEDIA_FOREVER_GET_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + if (is_string($result)) { + $json = json_decode($result, true); + if ($json) { + if (isset($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } else { + return $result; + } + } + return $result; + } + return false; + } + + /** + * 删除永久素材(认证后的订阅号可用) + * @param string $media_id 媒体文件id + * @return bool + */ + public function delForeverMedia($media_id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('media_id' => $media_id); + $result = Tools::httpPost(self::API_URL_PREFIX . self::MEDIA_FOREVER_DEL_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 获取永久素材列表(认证后的订阅号可用) + * @param string $type 素材的类型,图片(image)、视频(video)、语音 (voice)、图文(news) + * @param int $offset 全部素材的偏移位置,0表示从第一个素材 + * @param int $count 返回素材的数量,取值在1到20之间 + * @return bool|array + * 返回数组格式: + * array( + * 'total_count'=>0, //该类型的素材的总数 + * 'item_count'=>0, //本次调用获取的素材的数量 + * 'item'=>array() //素材列表数组,内容定义请参考官方文档 + * ) + */ + public function getForeverList($type, $offset, $count) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array( + 'type' => $type, + 'offset' => $offset, + 'count' => $count, + ); + $result = Tools::httpPost(self::API_URL_PREFIX . self::MEDIA_FOREVER_BATCHGET_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (isset($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取永久素材总数(认证后的订阅号可用) + * @return bool|array + * 返回数组格式: + * array( + * 'voice_count'=>0, //语音总数量 + * 'video_count'=>0, //视频总数量 + * 'image_count'=>0, //图片总数量 + * 'news_count'=>0 //图文总数量 + * ) + */ + public function getForeverCount() { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_URL_PREFIX . self::MEDIA_FOREVER_COUNT_URL . "access_token={$this->access_token}"); + if ($result) { + $json = json_decode($result, true); + if (isset($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 上传图文消息素材,用于群发(认证后的订阅号可用) + * @param array $data 消息结构{"articles":[{...}]} + * @return bool|array + */ + public function uploadArticles($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::MEDIA_UPLOADNEWS_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 上传视频素材(认证后的订阅号可用) + * @param array $data 消息结构 + * { + * "media_id"=>"", //通过上传媒体接口得到的MediaId + * "title"=>"TITLE", //视频标题 + * "description"=>"Description" //视频描述 + * } + * @return bool|array + * { + * "type":"video", + * "media_id":"mediaid", + * "created_at":1398848981 + * } + */ + public function uploadMpVideo($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::UPLOAD_MEDIA_URL . self::MEDIA_VIDEO_UPLOAD . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + +} diff --git a/core/extend/Wechat/WechatMenu.php b/core/extend/Wechat/WechatMenu.php new file mode 100644 index 00000000..e8704562 --- /dev/null +++ b/core/extend/Wechat/WechatMenu.php @@ -0,0 +1,164 @@ + + * @date 2016/06/28 11:52 + */ +class WechatMenu extends Common { + + /** 创建自定义菜单 */ + const MENU_ADD_URL = '/menu/create?'; + /* 获取自定义菜单 */ + const MENU_GET_URL = '/menu/get?'; + /* 删除自定义菜单 */ + const MENU_DEL_URL = '/menu/delete?'; + + /** 添加个性菜单 */ + const COND_MENU_ADD_URL = '/menu/addconditional?'; + /* 删除个性菜单 */ + const COND_MENU_DEL_URL = '/menu/delconditional?'; + /* 测试个性菜单 */ + const COND_MENU_TRY_URL = '/menu/trymatch?'; + + /** + * 创建自定义菜单 + * @param array $data 菜单数组数据 + * @link https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141013&token=&lang=zh_CN 文档 + * @return bool + */ + public function createMenu($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::MENU_ADD_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 获取所有菜单 + * @return bool|array + */ + public function getMenu() { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_URL_PREFIX . self::MENU_GET_URL . "access_token={$this->access_token}"); + if ($result) { + $json = json_decode($result, true); + if (!$json || isset($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 删除所有菜单 + * @return bool + */ + public function deleteMenu() { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_URL_PREFIX . self::MENU_DEL_URL . "access_token={$this->access_token}"); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 创建个性菜单 + * @param array $data 菜单数组数据 + * @link https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1455782296&token=&lang=zh_CN 文档 + * @return bool|string + */ + public function createCondMenu($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::COND_MENU_ADD_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode']) || empty($json['menuid'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json['menuid']; + } + return false; + } + + /** + * 删除个性菜单 + * @param string $menuid 菜单ID + * @return bool + */ + public function deleteCondMenu($menuid) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('menuid' => $menuid); + $result = Tools::httpPost(self::API_URL_PREFIX . self::COND_MENU_DEL_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 测试并返回个性化菜单 + * @param string $openid 粉丝openid + * @return bool + */ + public function tryCondMenu($openid) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('user_id' => $openid); + $result = Tools::httpPost(self::API_URL_PREFIX . self::COND_MENU_TRY_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + +} diff --git a/core/extend/Wechat/WechatOauth.php b/core/extend/Wechat/WechatOauth.php new file mode 100644 index 00000000..bbef2ea8 --- /dev/null +++ b/core/extend/Wechat/WechatOauth.php @@ -0,0 +1,120 @@ +appid}&redirect_uri={$redirect_uri}&response_type=code&scope={$scope}&state={$state}#wechat_redirect"; + } + + /** + * 通过 code 获取 AccessToken 和 openid + * @return bool|array + */ + public function getOauthAccessToken() { + $code = isset($_GET['code']) ? $_GET['code'] : ''; + if (empty($code)) { + Tools::log("getOauthAccessToken Fail, Because there is no access to the code value in get."); + return false; + } + $result = Tools::httpGet(self::API_BASE_URL_PREFIX . self::OAUTH_TOKEN_URL . "appid={$this->appid}&secret={$this->appsecret}&code={$code}&grant_type=authorization_code"); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + Tools::log("WechatOauth::getOauthAccessToken Fail.{$this->errMsg} [{$this->errCode}]", 'ERR'); + return false; + } + return $json; + } + return false; + } + + /** + * 刷新access token并续期 + * @param string $refresh_token + * @return bool|array + */ + public function getOauthRefreshToken($refresh_token) { + $result = Tools::httpGet(self::API_BASE_URL_PREFIX . self::OAUTH_REFRESH_URL . "appid={$this->appid}&grant_type=refresh_token&refresh_token={$refresh_token}"); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + Tools::log("WechatOauth::getOauthRefreshToken Fail.{$this->errMsg} [{$this->errCode}]", 'ERR'); + return false; + } + return $json; + } + return false; + } + + /** + * 获取授权后的用户资料 + * @param string $access_token + * @param string $openid + * @return bool|array {openid,nickname,sex,province,city,country,headimgurl,privilege,[unionid]} + * 注意:unionid字段 只有在用户将公众号绑定到微信开放平台账号后,才会出现。建议调用前用isset()检测一下 + */ + public function getOauthUserInfo($access_token, $openid) { + $result = Tools::httpGet(self::API_BASE_URL_PREFIX . self::OAUTH_USERINFO_URL . "access_token={$access_token}&openid={$openid}"); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + Tools::log("WechatOauth::getOauthUserInfo Fail.{$this->errMsg} [{$this->errCode}]", 'ERR'); + return false; + } + return $json; + } + return false; + } + + /** + * 检验授权凭证是否有效 + * @param string $access_token + * @param string $openid + * @return bool 是否有效 + */ + public function getOauthAuth($access_token, $openid) { + $result = Tools::httpGet(self::API_BASE_URL_PREFIX . self::OAUTH_AUTH_URL . "access_token={$access_token}&openid={$openid}"); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + Tools::log("WechatOauth::getOauthAuth Fail.{$this->errMsg} [{$this->errCode}]", 'ERR'); + return false; + } else if ($json['errcode'] == 0) { + return true; + } + } + return false; + } + +} diff --git a/core/extend/Wechat/WechatPay.php b/core/extend/Wechat/WechatPay.php new file mode 100644 index 00000000..8be39107 --- /dev/null +++ b/core/extend/Wechat/WechatPay.php @@ -0,0 +1,532 @@ + + * @date 2015/05/13 12:12:00 + */ +class WechatPay { + + /** 支付接口基础地址 */ + const MCH_BASE_URL = 'https://api.mch.weixin.qq.com'; + + /** 公众号appid */ + public $appid; + + /** 商户身份ID */ + public $mch_id; + + /** 商户支付密钥Key */ + public $partnerKey; + + /** 证书路径 */ + public $ssl_cer; + public $ssl_key; + + /** 执行错误消息及代码 */ + public $errMsg; + public $errCode; + + /** + * WechatPay constructor. + * @param array $options + */ + public function __construct($options = array()) { + $config = Loader::config($options); + $this->appid = isset($config['appid']) ? $config['appid'] : ''; + $this->mch_id = isset($config['mch_id']) ? $config['mch_id'] : ''; + $this->partnerKey = isset($config['partnerkey']) ? $config['partnerkey'] : ''; + $this->ssl_cer = isset($config['ssl_cer']) ? $config['ssl_cer'] : ''; + $this->ssl_key = isset($config['ssl_key']) ? $config['ssl_key'] : ''; + } + + /** + * 设置标配的请求参数,生成签名,生成接口参数xml + * @param array $data + * @return string + */ + protected function createXml($data) { + if (!isset($data['wxappid']) && !isset($data['mch_appid']) && !isset($data['appid'])) { + $data['appid'] = $this->appid; + } + if (!isset($data['mchid']) && !isset($data['mch_id'])) { + $data['mch_id'] = $this->mch_id; + } + isset($data['nonce_str']) || $data['nonce_str'] = Tools::createNoncestr(); + $data["sign"] = Tools::getPaySign($data, $this->partnerKey); + return Tools::arr2xml($data); + } + + /** + * POST提交XML + * @param array $data + * @param string $url + * @return mixed + */ + public function postXml($data, $url) { + return Tools::httpPost($url, $this->createXml($data)); + } + + /** + * 使用证书post请求XML + * @param array $data + * @param string $url + * @return mixed + */ + function postXmlSSL($data, $url) { + return Tools::httpsPost($url, $this->createXml($data), $this->ssl_cer, $this->ssl_key); + } + + /** + * POST提交获取Array结果 + * @param array $data 需要提交的数据 + * @param string $url + * @param string $method + * @return array + */ + public function getArrayResult($data, $url, $method = 'postXml') { + return Tools::xml2arr($this->$method($data, $url)); + } + + /** + * 解析返回的结果 + * @param array $result + * @return bool|array + */ + protected function _parseResult($result) { + if (empty($result)) { + $this->errCode = 'result error'; + $this->errMsg = '解析返回结果失败'; + return false; + } + if ($result['return_code'] !== 'SUCCESS') { + $this->errCode = $result['return_code']; + $this->errMsg = $result['return_msg']; + return false; + } + if (isset($result['err_code']) && $result['err_code'] !== 'SUCCESS') { + $this->errMsg = $result['err_code_des']; + $this->errCode = $result['err_code']; + return false; + } + return $result; + } + + /** + * 创建刷卡支付参数包 + * @param string $auth_code 授权Code号 + * @param string $out_trade_no 商户订单号 + * @param int $total_fee 支付费用 + * @param string $body 订单标识 + * @param null $goods_tag 商品标签 + * @return array|bool + */ + public function createMicroPay($auth_code, $out_trade_no, $total_fee, $body, $goods_tag = null) { + $data = array( + "appid" => $this->appid, + "mch_id" => $this->mch_id, + "body" => $body, + "out_trade_no" => $out_trade_no, + "total_fee" => $total_fee, + "auth_code" => $auth_code, + "spbill_create_ip" => Tools::getAddress() + ); + empty($goods_tag) || $data['goods_tag'] = $goods_tag; + $json = Tools::xml2arr($this->postXml($data, self::MCH_BASE_URL . '/pay/micropay')); + if (!empty($json) && false === $this->_parseResult($json)) { + return false; + } + return $json; + } + + /** + * 支付通知验证处理 + * @return bool|array + */ + public function getNotify() { + $notifyInfo = (array)simplexml_load_string(file_get_contents("php://input"), 'SimpleXMLElement', LIBXML_NOCDATA); + if (empty($notifyInfo)) { + Tools::log('Payment notification forbidden access.', 'ERR'); + $this->errCode = '404'; + $this->errMsg = 'Payment notification forbidden access.'; + return false; + } + if (empty($notifyInfo['sign'])) { + Tools::log('Payment notification signature is missing.' . var_export($notifyInfo, true), 'ERR'); + $this->errCode = '403'; + $this->errMsg = 'Payment notification signature is missing.'; + return false; + } + $data = $notifyInfo; + unset($data['sign']); + if ($notifyInfo['sign'] !== Tools::getPaySign($data, $this->partnerKey)) { + Tools::log('Payment notification signature verification failed.' . var_export($notifyInfo, true), 'ERR'); + $this->errCode = '403'; + $this->errMsg = 'Payment signature verification failed.'; + return false; + } + Tools::log('Payment notification signature verification success.' . var_export($notifyInfo, true), 'MSG'); + $this->errCode = '0'; + $this->errMsg = ''; + return $notifyInfo; + } + + + /** + * 支付XML统一回复 + * @param array $data 需要回复的XML内容数组 + * @param bool $isReturn 是否返回XML内容,默认不返回 + * @return string + */ + public function replyXml(array $data, $isReturn = false) { + $xml = Tools::arr2xml($data); + if ($isReturn) { + return $xml; + } + ob_clean(); + exit($xml); + } + + /** + * 获取预支付ID + * @param string $openid 用户openid,JSAPI必填 + * @param string $body 商品标题 + * @param string $out_trade_no 第三方订单号 + * @param int $total_fee 订单总价 + * @param string $notify_url 支付成功回调地址 + * @param string $trade_type 支付类型JSAPI|NATIVE|APP + * @param string $goods_tag 商品标记,代金券或立减优惠功能的参数 + * @return bool|string + */ + public function getPrepayId($openid, $body, $out_trade_no, $total_fee, $notify_url, $trade_type = "JSAPI", $goods_tag = null) { + $postdata = array( + "body" => $body, + "out_trade_no" => $out_trade_no, + "total_fee" => $total_fee, + "notify_url" => $notify_url, + "trade_type" => $trade_type, + "spbill_create_ip" => Tools::getAddress() + ); + empty($goods_tag) || $postdata['goods_tag'] = $goods_tag; + empty($openid) || $postdata['openid'] = $openid; + $result = $this->getArrayResult($postdata, self::MCH_BASE_URL . '/pay/unifiedorder'); + if (false === $this->_parseResult($result)) { + return false; + } + return in_array($trade_type, array('JSAPI', 'APP')) ? $result['prepay_id'] : $result['code_url']; + } + + /** + * 获取二维码预支付ID + * @param string $openid 用户openid,JSAPI必填 + * @param string $body 商品标题 + * @param string $out_trade_no 第三方订单号 + * @param int $total_fee 订单总价 + * @param string $notify_url 支付成功回调地址 + * @param string $goods_tag 商品标记,代金券或立减优惠功能的参数 + * @return bool|string + */ + public function getQrcPrepayId($openid, $body, $out_trade_no, $total_fee, $notify_url, $goods_tag = null) { + $postdata = array( + "body" => $body, + "out_trade_no" => $out_trade_no, + "total_fee" => $total_fee, + "notify_url" => $notify_url, + "trade_type" => 'NATIVE', + "spbill_create_ip" => Tools::getAddress() + ); + empty($goods_tag) || $postdata['goods_tag'] = $goods_tag; + empty($openid) || $postdata['openid'] = $openid; + $result = $this->getArrayResult($postdata, self::MCH_BASE_URL . '/pay/unifiedorder'); + if (false === $this->_parseResult($result) || empty($result['prepay_id'])) { + return false; + } + return $result['prepay_id']; + } + + /** + * 获取支付规二维码 + * @param string $product_id 商户定义的商品id 或者订单号 + * @return string + */ + public function getQrcPayUrl($product_id) { + $data = array( + 'appid' => $this->appid, + 'mch_id' => $this->mch_id, + 'time_stamp' => (string)time(), + 'nonce_str' => Tools::createNoncestr(), + 'product_id' => (string)$product_id, + ); + $data['sign'] = Tools::getPaySign($data, $this->partnerKey); + return "weixin://wxpay/bizpayurl?" . http_build_query($data); + } + + + /** + * 创建JSAPI支付参数包 + * @param string $prepay_id + * @return array + */ + public function createMchPay($prepay_id) { + $option = array(); + $option["appId"] = $this->appid; + $option["timeStamp"] = (string)time(); + $option["nonceStr"] = Tools::createNoncestr(); + $option["package"] = "prepay_id={$prepay_id}"; + $option["signType"] = "MD5"; + $option["paySign"] = Tools::getPaySign($option, $this->partnerKey); + $option['timestamp'] = $option['timeStamp']; + return $option; + } + + /** + * 关闭订单 + * @param string $out_trade_no + * @return bool + */ + public function closeOrder($out_trade_no) { + $data = array('out_trade_no' => $out_trade_no); + $result = $this->getArrayResult($data, self::MCH_BASE_URL . '/pay/closeorder'); + if (false === $this->_parseResult($result)) { + return false; + } + return ($result['return_code'] === 'SUCCESS'); + } + + /** + * 查询订单详情 + * @param $out_trade_no + * @return bool|array + */ + public function queryOrder($out_trade_no) { + $data = array('out_trade_no' => $out_trade_no); + $result = $this->getArrayResult($data, self::MCH_BASE_URL . '/pay/orderquery'); + if (false === $this->_parseResult($result)) { + return false; + } + return $result; + } + + /** + * 订单退款接口 + * @param string $out_trade_no 商户订单号 + * @param string $transaction_id 微信订单号 + * @param string $out_refund_no 商户退款订单号 + * @param int $total_fee 商户订单总金额 + * @param int $refund_fee 退款金额 + * @param int|null $op_user_id 操作员ID,默认商户ID + * @param string $refund_account 退款资金来源 + * 仅针对老资金流商户使用 + * REFUND_SOURCE_UNSETTLED_FUNDS --- 未结算资金退款(默认使用未结算资金退款) + * REFUND_SOURCE_RECHARGE_FUNDS --- 可用余额退款 + * @return bool + */ + public function refund($out_trade_no, $transaction_id, $out_refund_no, $total_fee, $refund_fee, $op_user_id = null, $refund_account = '') { + $data = array(); + $data['out_trade_no'] = $out_trade_no; + $data['transaction_id'] = $transaction_id; + $data['out_refund_no'] = $out_refund_no; + $data['total_fee'] = $total_fee; + $data['refund_fee'] = $refund_fee; + $data['op_user_id'] = empty($op_user_id) ? $this->mch_id : $op_user_id; + !empty($refund_account) && $data['refund_account'] = $refund_account; + $result = $this->getArrayResult($data, self::MCH_BASE_URL . '/secapi/pay/refund', 'postXmlSSL'); + if (false === $this->_parseResult($result)) { + return false; + } + return ($result['return_code'] === 'SUCCESS'); + } + + /** + * 退款查询接口 + * @param string $out_trade_no + * @return bool|array + */ + public function refundQuery($out_trade_no) { + $data = array(); + $data['out_trade_no'] = $out_trade_no; + $result = $this->getArrayResult($data, self::MCH_BASE_URL . '/pay/refundquery'); + if (false === $this->_parseResult($result)) { + return false; + } + return $result; + } + + /** + * 获取对账单 + * @param string $bill_date 账单日期,如 20141110 + * @param string $bill_type ALL|SUCCESS|REFUND|REVOKED + * @return bool|array + */ + public function getBill($bill_date, $bill_type = 'ALL') { + $data = array(); + $data['bill_date'] = $bill_date; + $data['bill_type'] = $bill_type; + $result = $this->postXml($data, self::MCH_BASE_URL . '/pay/downloadbill'); + $json = Tools::xml2arr($result); + if (!empty($json) && false === $this->_parseResult($json)) { + return false; + } + return $json; + } + + /** + * 发送现金红包 + * @param string $openid 红包接收者OPENID + * @param int $total_amount 红包总金额 + * @param string $mch_billno 商户订单号 + * @param string $sendname 商户名称 + * @param string $wishing 红包祝福语 + * @param string $act_name 活动名称 + * @param string $remark 备注信息 + * @param null|int $total_num 红包发放总人数(大于1为裂变红包) + * @param null|string $scene_id 场景id + * @param string $risk_info 活动信息 + * @param null|string $consume_mch_id 资金授权商户号 + * @return array|bool + * @link https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_5 + */ + public function sendRedPack($openid, $total_amount, $mch_billno, $sendname, $wishing, $act_name, $remark, $total_num = 1, $scene_id = null, $risk_info = '', $consume_mch_id = null) { + $data = array(); + $data['mch_billno'] = $mch_billno; // 商户订单号 mch_id+yyyymmdd+10位一天内不能重复的数字 + $data['wxappid'] = $this->appid; + $data['send_name'] = $sendname; //商户名称 + $data['re_openid'] = $openid; //红包接收者 + $data['total_amount'] = $total_amount; //红包总金额 + $data['total_num'] = '1'; //发放人数据 + $data['wishing'] = $wishing; //红包祝福语 + $data['client_ip'] = Tools::getAddress(); //调用接口的机器Ip地址 + $data['act_name'] = $act_name; //活动名称 + $data['remark'] = $remark; //备注信息 + $data['total_num'] = $total_num; + !empty($scene_id) && $data['scene_id'] = $scene_id; + !empty($risk_info) && $data['risk_info'] = $risk_info; + !empty($consume_mch_id) && $data['consume_mch_id'] = $consume_mch_id; + if ($total_num > 1) { + $data['amt_type'] = 'ALL_RAND'; + $api = self::MCH_BASE_URL . '/mmpaymkttransfers/sendgroupredpack'; + } else { + $api = self::MCH_BASE_URL . '/mmpaymkttransfers/sendredpack'; + } + $result = $this->postXmlSSL($data, $api); + $json = Tools::xml2arr($result); + if (!empty($json) && false === $this->_parseResult($json)) { + return false; + } + return $json; + } + + + /** + * 现金红包状态查询 + * @param string $billno + * @return bool|array + * @link https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_7&index=6 + */ + public function queryRedPack($billno) { + $data['mch_billno'] = $billno; + $data['bill_type'] = 'MCHT'; + $result = $this->postXmlSSL($data, self::MCH_BASE_URL . '/mmpaymkttransfers/gethbinfo'); + $json = Tools::xml2arr($result); + if (!empty($json) && false === $this->_parseResult($json)) { + return false; + } + return $json; + } + + /** + * 企业付款 + * @param string $openid 红包接收者OPENID + * @param int $amount 红包总金额 + * @param string $billno 商户订单号 + * @param string $desc 备注信息 + * @return bool|array + * @link https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 + */ + public function transfers($openid, $amount, $billno, $desc) { + $data = array(); + $data['mchid'] = $this->mch_id; + $data['mch_appid'] = $this->appid; + $data['partner_trade_no'] = $billno; + $data['openid'] = $openid; + $data['amount'] = $amount; + $data['check_name'] = 'NO_CHECK'; #不验证姓名 + $data['spbill_create_ip'] = Tools::getAddress(); //调用接口的机器Ip地址 + $data['desc'] = $desc; //备注信息 + $result = $this->postXmlSSL($data, self::MCH_BASE_URL . '/mmpaymkttransfers/promotion/transfers'); + $json = Tools::xml2arr($result); + if (!empty($json) && false === $this->_parseResult($json)) { + return false; + } + return $json; + } + + /** + * 企业付款查询 + * @param string $billno + * @return bool|array + * @link https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_3 + */ + public function queryTransfers($billno) { + $data['appid'] = $this->appid; + $data['mch_id'] = $this->mch_id; + $data['partner_trade_no'] = $billno; + $result = $this->postXmlSSL($data, self::MCH_BASE_URL . '/mmpaymkttransfers/gettransferinfo'); + $json = Tools::xml2arr($result); + if (!empty($json) && false === $this->_parseResult($json)) { + return false; + } + return $json; + } + + /** + * 二维码链接转成短链接 + * @param string $url 需要处理的长链接 + * @return bool|string + */ + public function shortUrl($url) { + $data = array(); + $data['long_url'] = $url; + $result = $this->getArrayResult($data, self::MCH_BASE_URL . '/tools/shorturl'); + if (!$result || $result['return_code'] !== 'SUCCESS') { + $this->errCode = $result['return_code']; + $this->errMsg = $result['return_msg']; + return false; + } + if (isset($result['err_code']) && $result['err_code'] !== 'SUCCESS') { + $this->errMsg = $result['err_code_des']; + $this->errCode = $result['err_code']; + return false; + } + return $result['short_url']; + } + + /** + * 发放代金券 + * @param int $coupon_stock_id 代金券批次id + * @param string $partner_trade_no 商户此次发放凭据号(格式:商户id+日期+流水号),商户侧需保持唯一性 + * @param string $openid Openid信息 + * @param string $op_user_id 操作员帐号, 默认为商户号 可在商户平台配置操作员对应的api权限 + * @return bool|array + * @link https://pay.weixin.qq.com/wiki/doc/api/tools/sp_coupon.php?chapter=12_3 + */ + public function sendCoupon($coupon_stock_id, $partner_trade_no, $openid, $op_user_id = null) { + $data = array(); + $data['appid'] = $this->appid; + $data['coupon_stock_id'] = $coupon_stock_id; + $data['openid_count'] = 1; + $data['partner_trade_no'] = $partner_trade_no; + $data['openid'] = $openid; + $data['op_user_id'] = empty($op_user_id) ? $this->mch_id : $op_user_id; + $result = $this->postXmlSSL($data, self::MCH_BASE_URL . '/mmpaymkttransfers/send_coupon'); + $json = Tools::xml2arr($result); + if (!empty($json) && false === $this->_parseResult($json)) { + return false; + } + return $json; + } +} diff --git a/core/extend/Wechat/WechatPoi.php b/core/extend/Wechat/WechatPoi.php new file mode 100644 index 00000000..1bce669f --- /dev/null +++ b/core/extend/Wechat/WechatPoi.php @@ -0,0 +1,174 @@ + + * @date 2016/10/26 15:43 + */ +class WechatPoi extends Common { + + /** 创建门店 */ + const POI_ADD = '/cgi-bin/poi/addpoi?'; + + /** 查询门店信息 */ + const POI_GET = '/cgi-bin/poi/getpoi?'; + + /** 获取门店列表 */ + const POI_GET_LIST = '/cgi-bin/poi/getpoilist?'; + + /** 修改门店信息 */ + const POI_UPDATE = '/cgi-bin/poi/updatepoi?'; + + /** 删除门店 */ + const POI_DELETE = '/cgi-bin/poi/delpoi?'; + + /** 获取门店类目表 */ + const POI_CATEGORY = '/cgi-bin/poi/getwxcategory?'; + + /** + * 创建门店 + * @link https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1444378120&token=&lang=zh_CN + * @param array $data + * @return bool + */ + public function addPoi($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::POI_ADD . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 删除门店 + * @link https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1444378120&token=&lang=zh_CN + * @param string $poi_id JSON数据格式 + * @return bool|array + */ + public function delPoi($poi_id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('poi_id' => $poi_id); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::POI_DELETE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 修改门店服务信息 + * @link https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1444378120&token=&lang=zh_CN + * @param array $data + * @return bool + */ + public function updatePoi($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::POI_UPDATE . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 查询门店信息 + * @link https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1444378120&token=&lang=zh_CN + * @param string $poi_id + * @return bool + */ + public function getPoi($poi_id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('poi_id' => $poi_id); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::POI_GET . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 查询门店列表 + * @link https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1444378120&token=&lang=zh_CN + * @param int $begin 开始位置,0 即为从第一条开始查询 + * @param int $limit 返回数据条数,最大允许50,默认为20 + * @return bool|array + */ + public function getPoiList($begin = 0, $limit = 50) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $limit > 50 && $limit = 50; + $data = array('begin' => $begin, 'limit' => $limit); + $result = Tools::httpPost(self::API_BASE_URL_PREFIX . self::POI_GET_LIST . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取商家门店类目表 + * @return bool|string + */ + public function getCategory() { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_URL_PREFIX . self::POI_CATEGORY . "access_token={$this->access_token}"); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + +} diff --git a/core/extend/Wechat/WechatReceive.php b/core/extend/Wechat/WechatReceive.php new file mode 100644 index 00000000..1c33199b --- /dev/null +++ b/core/extend/Wechat/WechatReceive.php @@ -0,0 +1,993 @@ + + * @date 2016/06/28 11:29 + */ +class WechatReceive extends Common { + + /** 消息推送地址 */ + const CUSTOM_SEND_URL = '/message/custom/send?'; + const MASS_SEND_URL = '/message/mass/send?'; + const TEMPLATE_SET_INDUSTRY_URL = '/message/template/api_set_industry?'; + const TEMPLATE_ADD_TPL_URL = '/message/template/api_add_template?'; + const TEMPLATE_SEND_URL = '/message/template/send?'; + const MASS_SEND_GROUP_URL = '/message/mass/sendall?'; + const MASS_DELETE_URL = '/message/mass/delete?'; + const MASS_PREVIEW_URL = '/message/mass/preview?'; + const MASS_QUERY_URL = '/message/mass/get?'; + + /** 消息回复类型 */ + const MSGTYPE_TEXT = 'text'; + const MSGTYPE_IMAGE = 'image'; + const MSGTYPE_LOCATION = 'location'; + const MSGTYPE_LINK = 'link'; + const MSGTYPE_EVENT = 'event'; + const MSGTYPE_MUSIC = 'music'; + const MSGTYPE_NEWS = 'news'; + const MSGTYPE_VOICE = 'voice'; + const MSGTYPE_VIDEO = 'video'; + + /** 文件过滤 */ + protected $_text_filter = true; + + /** 消息对象 */ + private $_receive; + + /** + * 获取微信服务器发来的内容 + * @return $this + */ + public function getRev() { + if ($this->_receive) { + return $this; + } + $postStr = !empty($this->postxml) ? $this->postxml : file_get_contents("php://input"); + !empty($postStr) && $this->_receive = (array)simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA); + return $this; + } + + /** + * 获取微信服务器发来的信息数据 + * @return array + */ + public function getRevData() { + return $this->_receive; + } + + /** + * 获取消息发送者 + * @return bool|string + */ + public function getRevFrom() { + if (isset($this->_receive['FromUserName'])) { + return $this->_receive['FromUserName']; + } + return false; + } + + /** + * 获取消息接受者 + * @return bool|string + */ + public function getRevTo() { + if (isset($this->_receive['ToUserName'])) { + return $this->_receive['ToUserName']; + } + return false; + } + + /** + * 获取接收消息的类型 + * @return bool|string + */ + public function getRevType() { + if (isset($this->_receive['MsgType'])) { + return $this->_receive['MsgType']; + } + return false; + } + + /** + * 获取消息ID + * @return bool|string + */ + public function getRevID() { + if (isset($this->_receive['MsgId'])) { + return $this->_receive['MsgId']; + } + return false; + } + + /** + * 获取消息发送时间 + * @return bool|string + */ + public function getRevCtime() { + if (isset($this->_receive['CreateTime'])) { + return $this->_receive['CreateTime']; + } + return false; + } + + /** + * 获取卡券事件推送 - 卡卷审核是否通过 + * 当Event为 card_pass_check(审核通过) 或 card_not_pass_check(未通过) + * @return bool|string 返回卡券ID + */ + public function getRevCardPass() { + if (isset($this->_receive['CardId'])) { + return $this->_receive['CardId']; + } + return false; + } + + /** + * 获取卡券事件推送 - 领取卡券 + * 当Event为 user_get_card(用户领取卡券) + * @return bool|array + */ + public function getRevCardGet() { + $array = array(); + if (isset($this->_receive['CardId'])) { + $array['CardId'] = $this->_receive['CardId']; + } + if (isset($this->_receive['IsGiveByFriend'])) { + $array['IsGiveByFriend'] = $this->_receive['IsGiveByFriend']; + } + $array['OldUserCardCode'] = $this->_receive['OldUserCardCode']; + if (isset($this->_receive['UserCardCode']) && !empty($this->_receive['UserCardCode'])) { + $array['UserCardCode'] = $this->_receive['UserCardCode']; + } + if (isset($array) && count($array) > 0) { + return $array; + } + return false; + } + + /** + * 获取卡券事件推送 - 删除卡券 + * 当Event为 user_del_card(用户删除卡券) + * @return bool|array + */ + public function getRevCardDel() { + if (isset($this->_receive['CardId'])) { //卡券 ID + $array['CardId'] = $this->_receive['CardId']; + } + if (isset($this->_receive['UserCardCode']) && !empty($this->_receive['UserCardCode'])) { + $array['UserCardCode'] = $this->_receive['UserCardCode']; + } + if (isset($array) && count($array) > 0) { + return $array; + } + return false; + } + + /** + * 获取接收消息内容正文 + * @return bool + */ + public function getRevContent() { + if (isset($this->_receive['Content'])) { + return $this->_receive['Content']; + } else if (isset($this->_receive['Recognition'])) { //获取语音识别文字内容,需申请开通 + return $this->_receive['Recognition']; + } + return false; + } + + /** + * 获取接收消息图片 + * @return array|bool + */ + public function getRevPic() { + if (isset($this->_receive['PicUrl'])) { + return array( + 'mediaid' => $this->_receive['MediaId'], + 'picurl' => (string)$this->_receive['PicUrl'], //防止picurl为空导致解析出错 + ); + } + return false; + } + + /** + * 获取接收消息链接 + * @return bool|array + */ + public function getRevLink() { + if (isset($this->_receive['Url'])) { + return array( + 'url' => $this->_receive['Url'], + 'title' => $this->_receive['Title'], + 'description' => $this->_receive['Description'] + ); + } + return false; + } + + /** + * 获取接收地理位置 + * @return bool|array + */ + public function getRevGeo() { + if (isset($this->_receive['Location_X'])) { + return array( + 'x' => $this->_receive['Location_X'], + 'y' => $this->_receive['Location_Y'], + 'scale' => $this->_receive['Scale'], + 'label' => $this->_receive['Label'] + ); + } + return false; + } + + /** + * 获取上报地理位置事件 + * @return bool|array + */ + public function getRevEventGeo() { + if (isset($this->_receive['Latitude'])) { + return array( + 'x' => $this->_receive['Latitude'], + 'y' => $this->_receive['Longitude'], + 'precision' => $this->_receive['Precision'], + ); + } + return false; + } + + /** + * 获取接收事件推送 + * @return bool|array + */ + public function getRevEvent() { + if (isset($this->_receive['Event'])) { + $array['event'] = $this->_receive['Event']; + } + if (isset($this->_receive['EventKey'])) { + $array['key'] = $this->_receive['EventKey']; + } + if (isset($array) && count($array) > 0) { + return $array; + } + return false; + } + + /** + * 获取自定义菜单的扫码推事件信息 + * + * 事件类型为以下两种时则调用此方法有效 + * Event 事件类型,scancode_push + * Event 事件类型,scancode_waitmsg + * @return bool|array + */ + public function getRevScanInfo() { + if (isset($this->_receive['ScanCodeInfo'])) { + if (!is_array($this->_receive['ScanCodeInfo'])) { + $array = (array)$this->_receive['ScanCodeInfo']; + $this->_receive['ScanCodeInfo'] = $array; + } else { + $array = $this->_receive['ScanCodeInfo']; + } + } + if (isset($array) && count($array) > 0) { + return $array; + } + return false; + } + + /** + * 获取自定义菜单的图片发送事件信息 + * + * 事件类型为以下三种时则调用此方法有效 + * Event 事件类型,pic_sysphoto 弹出系统拍照发图的事件推送 + * Event 事件类型,pic_photo_or_album 弹出拍照或者相册发图的事件推送 + * Event 事件类型,pic_weixin 弹出微信相册发图器的事件推送 + * + * @return bool|array + * array ( + * 'Count' => '2', + * 'PicList' =>array ( + * 'item' =>array ( + * 0 =>array ('PicMd5Sum' => 'aaae42617cf2a14342d96005af53624c'), + * 1 =>array ('PicMd5Sum' => '149bd39e296860a2adc2f1bb81616ff8'), + * ), + * ), + * ) + * + */ + public function getRevSendPicsInfo() { + if (isset($this->_receive['SendPicsInfo'])) { + if (!is_array($this->_receive['SendPicsInfo'])) { + $array = (array)$this->_receive['SendPicsInfo']; + if (isset($array['PicList'])) { + $array['PicList'] = (array)$array['PicList']; + $item = $array['PicList']['item']; + $array['PicList']['item'] = array(); + foreach ($item as $key => $value) { + $array['PicList']['item'][$key] = (array)$value; + } + } + $this->_receive['SendPicsInfo'] = $array; + } else { + $array = $this->_receive['SendPicsInfo']; + } + } + if (isset($array) && count($array) > 0) { + return $array; + } + return false; + } + + /** + * 获取自定义菜单的地理位置选择器事件推送 + * + * 事件类型为以下时则可以调用此方法有效 + * Event 事件类型,location_select 弹出地理位置选择器的事件推送 + * + * @return bool|array + * array ( + * 'Location_X' => '33.731655000061', + * 'Location_Y' => '113.29955200008047', + * 'Scale' => '16', + * 'Label' => '某某市某某区某某路', + * 'Poiname' => '', + * ) + * + */ + public function getRevSendGeoInfo() { + if (isset($this->_receive['SendLocationInfo'])) { + if (!is_array($this->_receive['SendLocationInfo'])) { + $array = (array)$this->_receive['SendLocationInfo']; + if (empty($array['Poiname'])) { + $array['Poiname'] = ""; + } + if (empty($array['Label'])) { + $array['Label'] = ""; + } + $this->_receive['SendLocationInfo'] = $array; + } else { + $array = $this->_receive['SendLocationInfo']; + } + } + if (isset($array) && count($array) > 0) { + return $array; + } + return false; + } + + /** + * 获取接收语音推送 + * @return bool|array + */ + public function getRevVoice() { + if (isset($this->_receive['MediaId'])) { + return array( + 'mediaid' => $this->_receive['MediaId'], + 'format' => $this->_receive['Format'], + ); + } + return false; + } + + /** + * 获取接收视频推送 + * @return array|bool + */ + public function getRevVideo() { + if (isset($this->_receive['MediaId'])) { + return array( + 'mediaid' => $this->_receive['MediaId'], + 'thumbmediaid' => $this->_receive['ThumbMediaId'] + ); + } + return false; + } + + /** + * 获取接收TICKET + * @return bool|string + */ + public function getRevTicket() { + if (isset($this->_receive['Ticket'])) { + return $this->_receive['Ticket']; + } + return false; + } + + /** + * 获取二维码的场景值 + * @return bool|string + */ + public function getRevSceneId() { + if (isset($this->_receive['EventKey'])) { + return str_replace('qrscene_', '', $this->_receive['EventKey']); + } + return false; + } + + /** + * 获取主动推送的消息ID + * 经过验证,这个和普通的消息MsgId不一样 + * 当Event为 MASSSENDJOBFINISH 或 TEMPLATESENDJOBFINISH + * @return bool|string + */ + public function getRevTplMsgID() { + if (isset($this->_receive['MsgID'])) { + return $this->_receive['MsgID']; + } + return false; + } + + /** + * 获取模板消息发送状态 + * @return bool|string + */ + public function getRevStatus() { + if (isset($this->_receive['Status'])) { + return $this->_receive['Status']; + } + return false; + } + + /** + * 获取群发或模板消息发送结果 + * 当Event为 MASSSENDJOBFINISH 或 TEMPLATESENDJOBFINISH,即高级群发/模板消息 + * @return bool|array + */ + public function getRevResult() { + if (isset($this->_receive['Status'])) { //发送是否成功,具体的返回值请参考 高级群发/模板消息 的事件推送说明 + $array['Status'] = $this->_receive['Status']; + } + if (isset($this->_receive['MsgID'])) { //发送的消息id + $array['MsgID'] = $this->_receive['MsgID']; + } + //以下仅当群发消息时才会有的事件内容 + if (isset($this->_receive['TotalCount'])) { //分组或openid列表内粉丝数量 + $array['TotalCount'] = $this->_receive['TotalCount']; + } + if (isset($this->_receive['FilterCount'])) { //过滤(过滤是指特定地区、性别的过滤、用户设置拒收的过滤,用户接收已超4条的过滤)后,准备发送的粉丝数 + $array['FilterCount'] = $this->_receive['FilterCount']; + } + if (isset($this->_receive['SentCount'])) { //发送成功的粉丝数 + $array['SentCount'] = $this->_receive['SentCount']; + } + if (isset($this->_receive['ErrorCount'])) { //发送失败的粉丝数 + $array['ErrorCount'] = $this->_receive['ErrorCount']; + } + if (isset($array) && count($array) > 0) { + return $array; + } + return false; + } + + /** + * 获取多客服会话状态推送事件 - 接入会话 + * 当Event为 kfcreatesession 即接入会话 + * @return bool|string + */ + public function getRevKFCreate() { + if (isset($this->_receive['KfAccount'])) { + return $this->_receive['KfAccount']; + } + return false; + } + + /** + * 获取多客服会话状态推送事件 - 关闭会话 + * 当Event为 kfclosesession 即关闭会话 + * @return bool|string + */ + public function getRevKFClose() { + if (isset($this->_receive['KfAccount'])) { + return $this->_receive['KfAccount']; + } + return false; + } + + /** + * 获取多客服会话状态推送事件 - 转接会话 + * 当Event为 kfswitchsession 即转接会话 + * @return bool|array + */ + public function getRevKFSwitch() { + if (isset($this->_receive['FromKfAccount'])) { //原接入客服 + $array['FromKfAccount'] = $this->_receive['FromKfAccount']; + } + if (isset($this->_receive['ToKfAccount'])) { //转接到客服 + $array['ToKfAccount'] = $this->_receive['ToKfAccount']; + } + if (isset($array) && count($array) > 0) { + return $array; + } + return false; + } + + /** + * 发送客服消息 + * @param array $data 消息结构{"touser":"OPENID","msgtype":"news","news":{...}} + * @return bool|array + */ + public function sendCustomMessage($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::CUSTOM_SEND_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 模板消息 设置所属行业 + * @param string $id1 公众号模板消息所属行业编号,参看官方开发文档 行业代码 + * @param string $id2 同$id1。但如果只有一个行业,此参数可省略 + * @return bool|mixed + */ + public function setTMIndustry($id1, $id2 = '') { + if ($id1) { + $data['industry_id1'] = $id1; + } + if ($id2) { + $data['industry_id2'] = $id2; + } + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::TEMPLATE_SET_INDUSTRY_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 模板消息 添加消息模板 + * 成功返回消息模板的调用id + * @param string $tpl_id 模板库中模板的编号,有“TM**”和“OPENTMTM**”等形式 + * @return bool|string + */ + public function addTemplateMessage($tpl_id) { + $data = array('template_id_short' => $tpl_id); + if (!$this->access_token && !$this->getAccessToken()) + return false; + $result = Tools::httpPost(self::API_URL_PREFIX . self::TEMPLATE_ADD_TPL_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json['template_id']; + } + return false; + } + + /** + * 发送模板消息 + * @param array $data 消息结构 + * { + * "touser":"OPENID", + * "template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY", + * "url":"http://weixin.qq.com/download", + * "topcolor":"#FF0000", + * "data":{ + * "参数名1": { + * "value":"参数", + * "color":"#173177" //参数颜色 + * }, + * "Date":{ + * "value":"06月07日 19时24分", + * "color":"#173177" + * }, + * "CardNumber":{ + * "value":"0426", + * "color":"#173177" + * }, + * "Type":{ + * "value":"消费", + * "color":"#173177" + * } + * } + * } + * @return bool|array + */ + public function sendTemplateMessage($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::TEMPLATE_SEND_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 转发多客服消息 + * @param string $customer_account + * @return $this + */ + public function transfer_customer_service($customer_account = '') { + $msg = array( + 'ToUserName' => $this->getRevFrom(), + 'FromUserName' => $this->getRevTo(), + 'CreateTime' => time(), + 'MsgType' => 'transfer_customer_service', + ); + if ($customer_account) { + $msg['TransInfo'] = array('KfAccount' => $customer_account); + } + $this->Message($msg); + return $this; + } + + /** + * 高级群发消息, 根据OpenID列表群发图文消息(订阅号不可用) + * 注意:视频需要在调用uploadMedia()方法后,再使用 uploadMpVideo() 方法生成, + * 然后获得的 mediaid 才能用于群发,且消息类型为 mpvideo 类型。 + * @param array $data 消息结构 + * { + * "touser"=>array( + * "OPENID1", + * "OPENID2" + * ), + * "msgtype"=>"mpvideo", + * // 在下面5种类型中选择对应的参数内容 + * // mpnews | voice | image | mpvideo => array( "media_id"=>"MediaId") + * // text => array ( "content" => "hello") + * } + * @return bool|array + */ + public function sendMassMessage($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::MASS_SEND_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 高级群发消息, 根据群组id群发图文消息(认证后的订阅号可用) + * 注意:视频需要在调用uploadMedia()方法后,再使用 uploadMpVideo() 方法生成, + * 然后获得的 mediaid 才能用于群发,且消息类型为 mpvideo 类型。 + * @param array $data 消息结构 + * { + * "filter"=>array( + * "is_to_all"=>False, //是否群发给所有用户.True不用分组id,False需填写分组id + * "group_id"=>"2" //群发的分组id + * ), + * "msgtype"=>"mpvideo", + * // 在下面5种类型中选择对应的参数内容 + * // mpnews | voice | image | mpvideo => array( "media_id"=>"MediaId") + * // text => array ( "content" => "hello") + * } + * @return bool|array + */ + public function sendGroupMassMessage($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::MASS_SEND_GROUP_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 高级群发消息, 删除群发图文消息(认证后的订阅号可用) + * @param string $msg_id 消息ID + * @return bool + */ + public function deleteMassMessage($msg_id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::MASS_DELETE_URL . "access_token={$this->access_token}", Tools::json_encode(array('msg_id' => $msg_id))); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return true; + } + return false; + } + + /** + * 高级群发消息, 预览群发消息(认证后的订阅号可用) + * 注意:视频需要在调用uploadMedia()方法后,再使用 uploadMpVideo() 方法生成, + * 然后获得的 mediaid 才能用于群发,且消息类型为 mpvideo 类型。 + * @param type $data + * @消息结构 + * { + * "touser"=>"OPENID", + * "msgtype"=>"mpvideo", + * // 在下面5种类型中选择对应的参数内容 + * // mpnews | voice | image | mpvideo => array( "media_id"=>"MediaId") + * // text => array ( "content" => "hello") + * } + * @return bool|array + */ + public function previewMassMessage($data) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::MASS_PREVIEW_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 高级群发消息, 查询群发消息发送状态(认证后的订阅号可用) + * @param string $msg_id 消息ID + * @return bool|array + * { + * "msg_id":201053012, //群发消息后返回的消息id + * "msg_status":"SEND_SUCCESS" //消息发送后的状态,SENDING表示正在发送 SEND_SUCCESS表示发送成功 + * } + */ + public function queryMassMessage($msg_id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::MASS_QUERY_URL . "access_token={$this->access_token}", Tools::json_encode(array('msg_id' => $msg_id))); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 设置发送消息 + * @param string|array $msg 消息数组 + * @param bool $append 是否在原消息数组追加 + * @return array + */ + public function Message($msg = '', $append = false) { + if (is_null($msg)) { + $this->_msg = array(); + } elseif (is_array($msg)) { + if ($append) { + $this->_msg = array_merge($this->_msg, $msg); + } else { + $this->_msg = $msg; + } + return $this->_msg; + } + return $this->_msg; + } + + /** + * 设置文本消息 + * @param string $text 文本内容 + * @return $this + */ + public function text($text = '') { + $msg = array( + 'ToUserName' => $this->getRevFrom(), + 'FromUserName' => $this->getRevTo(), + 'MsgType' => self::MSGTYPE_TEXT, + 'Content' => $this->_auto_text_filter($text), + 'CreateTime' => time(), + ); + $this->Message($msg); + return $this; + } + + /** + * 设置图片消息 + * @param string $mediaid 图片媒体ID + * @return $this + */ + public function image($mediaid = '') { + $msg = array( + 'ToUserName' => $this->getRevFrom(), + 'FromUserName' => $this->getRevTo(), + 'MsgType' => self::MSGTYPE_IMAGE, + 'Image' => array('MediaId' => $mediaid), + 'CreateTime' => time(), + ); + $this->Message($msg); + return $this; + } + + /** + * 设置语音回复消息 + * @param string $mediaid 语音媒体ID + * @return $this + */ + public function voice($mediaid = '') { + $msg = array( + 'ToUserName' => $this->getRevFrom(), + 'FromUserName' => $this->getRevTo(), + 'MsgType' => self::MSGTYPE_VOICE, + 'Voice' => array('MediaId' => $mediaid), + 'CreateTime' => time(), + ); + $this->Message($msg); + return $this; + } + + /** + * 设置视频回复消息 + * @param string $mediaid 视频媒体ID + * @param string $title 视频标题 + * @param string $description 视频描述 + * @return $this + */ + public function video($mediaid = '', $title = '', $description = '') { + $msg = array( + 'ToUserName' => $this->getRevFrom(), + 'FromUserName' => $this->getRevTo(), + 'MsgType' => self::MSGTYPE_VIDEO, + 'Video' => array( + 'MediaId' => $mediaid, + 'Title' => $title, + 'Description' => $description + ), + 'CreateTime' => time(), + ); + $this->Message($msg); + return $this; + } + + /** + * 设置音乐回复消息 + * @param string $title 音乐标题 + * @param string $desc 音乐描述 + * @param string $musicurl 音乐地址 + * @param string $hgmusicurl 高清音乐地址 + * @param string $thumbmediaid 音乐图片缩略图的媒体id(可选) + * @return $this + */ + public function music($title, $desc, $musicurl, $hgmusicurl = '', $thumbmediaid = '') { + $msg = array( + 'ToUserName' => $this->getRevFrom(), + 'FromUserName' => $this->getRevTo(), + 'CreateTime' => time(), + 'MsgType' => self::MSGTYPE_MUSIC, + 'Music' => array( + 'Title' => $title, + 'Description' => $desc, + 'MusicUrl' => $musicurl, + 'HQMusicUrl' => $hgmusicurl + ), + ); + if ($thumbmediaid) { + $msg['Music']['ThumbMediaId'] = $thumbmediaid; + } + $this->Message($msg); + return $this; + } + + /** + * 设置回复图文 + * @param array $newsData + * @return $this + */ + public function news($newsData = array()) { + $msg = array( + 'ToUserName' => $this->getRevFrom(), + 'FromUserName' => $this->getRevTo(), + 'CreateTime' => time(), + 'MsgType' => self::MSGTYPE_NEWS, + 'ArticleCount' => count($newsData), + 'Articles' => $newsData, + ); + $this->Message($msg); + return $this; + } + + /** + * 回复微信服务器 + * @param array $msg 要发送的信息(默认取$this->_msg) + * @param bool $return 是否返回信息而不抛出到浏览器(默认:否) + * @return bool|string + */ + public function reply($msg = array(), $return = false) { + if (empty($msg)) { + if (empty($this->_msg)) { //防止不先设置回复内容,直接调用reply方法导致异常 + return false; + } + $msg = $this->_msg; + } + $xmldata = Tools::arr2xml($msg); + if ($this->encrypt_type == 'aes') { //如果来源消息为加密方式 + !class_exists('Prpcrypt', FALSE) && require __DIR__ . '/Lib/Prpcrypt.php'; + $pc = new Prpcrypt($this->encodingAesKey); + // 如果是第三方平台,加密得使用 component_appid + $array = $pc->encrypt($xmldata, empty($this->config['component_appid']) ? $this->appid : $this->config['component_appid']); + $ret = $array[0]; + if ($ret != 0) { + Tools::log('encrypt err!'); + return false; + } + $timestamp = time(); + $nonce = rand(77, 999) * rand(605, 888) * rand(11, 99); + $encrypt = $array[1]; + $tmpArr = array($this->token, $timestamp, $nonce, $encrypt); //比普通公众平台多了一个加密的密文 + sort($tmpArr, SORT_STRING); + $signature = sha1(implode($tmpArr)); + $format = "%s"; + $xmldata = sprintf($format, $encrypt, $signature, $timestamp, $nonce); + } + if ($return) { + return $xmldata; + } + echo $xmldata; + } + + /** + * 过滤文字回复\r\n换行符 + * @param string $text + * @return string + */ + private function _auto_text_filter($text) { + if (!$this->_text_filter) { + return $text; + } + return str_replace("\r\n", "\n", $text); + } + +} diff --git a/core/extend/Wechat/WechatScript.php b/core/extend/Wechat/WechatScript.php new file mode 100644 index 00000000..e96120cb --- /dev/null +++ b/core/extend/Wechat/WechatScript.php @@ -0,0 +1,122 @@ + + * @date 2016/06/28 11:24 + */ +class WechatScript extends Common { + + /** + * JSAPI授权TICKET + * @var string + */ + public $jsapi_ticket; + + /** + * 删除JSAPI授权TICKET + * @param string $appid + * @return bool + */ + public function resetJsTicket($appid = '') { + $this->jsapi_ticket = ''; + $authname = 'wechat_jsapi_ticket_' . empty($appid) ? $this->appid : $appid; + Tools::removeCache($authname); + return true; + } + + /** + * 获取JSAPI授权TICKET + * @param string $appid 用于多个appid时使用,可空 + * @param string $jsapi_ticket 手动指定jsapi_ticket,非必要情况不建议用 + * @param string $access_token 获取 jsapi_ticket 指定 access_token + * @return bool|string + */ + public function getJsTicket($appid = '', $jsapi_ticket = '', $access_token = '') { + if (empty($access_token)) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $access_token = $this->access_token; + } + if (empty($appid)) { + $appid = $this->appid; + } + # 手动指定token,优先使用 + if ($jsapi_ticket) { + $this->jsapi_ticket = $jsapi_ticket; + return $this->jsapi_ticket; + } + # 尝试从缓存中读取 + $cache = 'wechat_jsapi_ticket_' . $appid; + $jt = Tools::getCache($cache); + if ($jt) { + return $this->jsapi_ticket = $jt; + } + # 检测事件注册 + if (isset(Loader::$callback[__FUNCTION__])) { + return $this->jsapi_ticket = call_user_func_array(Loader::$callback[__FUNCTION__], array(&$this, &$cache)); + } + # 调接口获取 + $result = Tools::httpGet(self::API_URL_PREFIX . self::GET_TICKET_URL . "access_token={$access_token}" . '&type=jsapi'); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + $this->jsapi_ticket = $json['ticket']; + Tools::setCache($cache, $this->jsapi_ticket, $json['expires_in'] ? intval($json['expires_in']) - 100 : 3600); + return $this->jsapi_ticket; + } + return false; + } + + /** + * 获取JsApi使用签名 + * @param string $url 网页的URL,自动处理#及其后面部分 + * @param int $timestamp 当前时间戳 (为空则自动生成) + * @param string $noncestr 随机串 (为空则自动生成) + * @param string $appid 用于多个appid时使用,可空 + * @param string $access_token 获取 jsapi_ticket 指定 access_token + * @return array|bool 返回签名字串 + */ + public function getJsSign($url, $timestamp = 0, $noncestr = '', $appid = '', $access_token = '') { + if (!$this->jsapi_ticket && !$this->getJsTicket($appid, '', $access_token) || empty($url)) { + return false; + } + $data = array( + "jsapi_ticket" => $this->jsapi_ticket, + "timestamp" => empty($timestamp) ? time() : $timestamp, + "noncestr" => '' . empty($noncestr) ? Tools::createNoncestr(16) : $noncestr, + "url" => trim($url), + ); + return array( + "url" => $url, + 'debug' => false, + "appId" => empty($appid) ? $this->appid : $appid, + "nonceStr" => $data['noncestr'], + "timestamp" => $data['timestamp'], + "signature" => Tools::getSignature($data, 'sha1'), + 'jsApiList' => array( + 'onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareWeibo', 'onMenuShareQZone', + 'hideOptionMenu', 'showOptionMenu', 'hideMenuItems', 'showMenuItems', 'hideAllNonBaseMenuItem', 'showAllNonBaseMenuItem', + 'chooseImage', 'previewImage', 'uploadImage', 'downloadImage', 'closeWindow', 'scanQRCode', 'chooseWXPay', + 'translateVoice', 'getNetworkType', 'openLocation', 'getLocation', + 'openProductSpecificView', 'addCard', 'chooseCard', 'openCard', + 'startRecord', 'stopRecord', 'onVoiceRecordEnd', 'playVoice', 'pauseVoice', 'stopVoice', 'onVoicePlayEnd', 'uploadVoice', 'downloadVoice', + 'openWXDeviceLib', 'closeWXDeviceLib', 'getWXDeviceInfos', 'sendDataToWXDevice', 'disconnectWXDevice', 'getWXDeviceTicket', 'connectWXDevice', + 'startScanWXDevice', 'stopScanWXDevice', 'onWXDeviceBindStateChange', 'onScanWXDeviceResult', 'onReceiveDataFromWXDevice', + 'onWXDeviceBluetoothStateChange', 'onWXDeviceStateChange' + ) + ); + } + +} diff --git a/core/extend/Wechat/WechatService.php b/core/extend/Wechat/WechatService.php new file mode 100644 index 00000000..8a16ad14 --- /dev/null +++ b/core/extend/Wechat/WechatService.php @@ -0,0 +1,382 @@ + + * @date 2016/10/18 00:35:55 + */ +class WechatService { + + const URL_PREFIX = 'https://api.weixin.qq.com/cgi-bin/component'; + // 获取服务access_token + const COMPONENT_TOKEN_URL = '/api_component_token'; + // 获取(刷新)授权公众号的令牌 + const REFRESH_ACCESS_TOKEN = '/api_authorizer_token'; + // 获取预授权码 + const PREAUTH_CODE_URL = '/api_create_preauthcode'; + // 获取公众号的授权信息 + const QUERY_AUTH_URL = '/api_query_auth'; + // 获取授权方的账户信息 + const GET_AUTHORIZER_INFO_URL = '/api_get_authorizer_info'; + // 获取授权方的选项设置信息 + const GET_AUTHORIZER_OPTION_URL = '/api_get_authorizer_option'; + // 设置授权方的选项信息 + const SET_AUTHORIZER_OPTION_URL = '/api_set_authorizer_option'; + + // 微信后台推送的ticket 每十分钟更新一次 + public $errCode; + // 服务appid + public $errMsg; + // 服务appsecret + protected $component_verify_ticket; + // 公众号消息校验Token + protected $component_appid; + // 公众号消息加解密Key + protected $component_appsecret; + // 服务令牌 + protected $component_token; + // 授权方appid + protected $component_encodingaeskey; + // 授权方令牌 + protected $component_access_token; + // 刷新令牌 + protected $authorizer_appid; + // JSON数据 + protected $pre_auth_code; + // 错误消息 + protected $data; + + /** + * WechatService constructor. + * @param array $options + */ + public function __construct($options = array()) { + $options = Loader::config($options); + $this->component_encodingaeskey = !empty($options['component_encodingaeskey']) ? $options['component_encodingaeskey'] : ''; + $this->component_verify_ticket = !empty($options['component_verify_ticket']) ? $options['component_verify_ticket'] : ''; + $this->component_appsecret = !empty($options['component_appsecret']) ? $options['component_appsecret'] : ''; + $this->component_token = !empty($options['component_token']) ? $options['component_token'] : ''; + $this->component_appid = !empty($options['component_appid']) ? $options['component_appid'] : ''; + } + + /** + * 接收公众平台推送的 Ticket + * @return bool|array + */ + public function getComonentTicket() { + $receive = new WechatReceive(array( + 'appid' => $this->component_appid, + 'appsecret' => $this->component_appsecret, + 'encodingaeskey' => $this->component_encodingaeskey, + 'token' => $this->component_token, + 'cachepath' => Cache::$cachepath + )); + # 会话内容解密状态判断 + if (false === $receive->valid()) { + $this->errCode = $receive->errCode; + $this->errMsg = $receive->errMsg; + Tools::log("Get Wechat Push ComponentVerifyTicket Faild. {$this->errMsg} [$this->errCode]", 'Err'); + return false; + } + $data = $receive->getRev()->getRevData(); + if ($data['InfoType'] === 'component_verify_ticket' && !empty($data['ComponentVerifyTicket'])) { + # 记录推送日志到微信SDK + Tools::log("Get Wechat Push ComponentVerifyTicket Success. "); + Tools::setCache('component_verify_ticket', $data['ComponentVerifyTicket']); + } + return $data; + } + + /** + * 获取(刷新)授权公众号的令牌 + * @注意1. 授权公众号访问access token2小时有效 + * @注意2. 一定保存好新的刷新令牌 + * @param string $authorizer_appid 授权方APPID + * @param string $authorizer_refresh_token 授权方刷新令牌 + * @return bool|string + */ + public function refreshAccessToken($authorizer_appid, $authorizer_refresh_token) { + empty($this->component_access_token) && $this->getComponentAccessToken(); + if (empty($this->component_access_token)) { + return false; + } + $data = array(); + $data['component_appid'] = $this->component_appid; + $data['authorizer_appid'] = $authorizer_appid; + $data['authorizer_refresh_token'] = $authorizer_refresh_token; + $url = self::URL_PREFIX . self::REFRESH_ACCESS_TOKEN . "?component_access_token={$this->component_access_token}"; + $result = Tools::httpPost($url, Tools::json_encode($data)); + if (($result = $this->_decode($result)) === false) { + Tools::log("Get getAuthorizerOption Faild. {$this->errMsg} [$this->errCode]", 'ERR'); + } + return $result; + } + + /** + * 获取或刷新服务 AccessToken + * @return bool|string + */ + public function getComponentAccessToken() { + $cacheKey = 'wechat_component_access_token'; + $this->component_access_token = Tools::getCache($cacheKey); + if (empty($this->component_access_token)) { + $data = array(); + $data['component_appid'] = $this->component_appid; + $data['component_appsecret'] = $this->component_appsecret; + $data['component_verify_ticket'] = $this->component_verify_ticket; + $url = self::URL_PREFIX . self::COMPONENT_TOKEN_URL; + $result = Tools::httpPost($url, Tools::json_encode($data)); + if (($this->component_access_token = $this->_decode($result, 'component_access_token')) === false) { + Tools::log("Get getComponentAccessToken Faild. {$this->errMsg} [$this->errCode]", 'ERR'); + return false; + } + Tools::setCache($cacheKey, $this->component_access_token, 7200); + } + return $this->component_access_token; + } + + /** + * 解析JSON数据 + * @param string $result + * @param string|null $field + * @return bool|array + */ + private function _decode($result, $field = null) { + $this->data = json_decode($result, true); + if (!empty($this->data['errcode'])) { + $this->errCode = $this->data['errcode']; + $this->errMsg = $this->data['errmsg']; + return false; + } + if ($this->data && !is_null($field)) { + if (isset($this->data[$field])) { + return $this->data[$field]; + } else { + return false; + } + } + return $this->data; + } + + /** + * 获取公众号的授权信息 + * + * @param string $authorization_code + * @return bool|array + */ + public function getAuthorizationInfo($authorization_code) { + empty($this->component_access_token) && $this->getComponentAccessToken(); + if (empty($this->component_access_token)) { + return false; + } + $data = array(); + $data['component_appid'] = $this->component_appid; + $data['authorization_code'] = $authorization_code; + $url = self::URL_PREFIX . self::QUERY_AUTH_URL . "?component_access_token={$this->component_access_token}"; + $result = Tools::httpPost($url, Tools::json_encode($data)); + $authorization_info = $this->_decode($result, 'authorization_info'); + if (empty($authorization_info)) { + Tools::log("Get getAuthorizationInfo Faild. {$this->errMsg} [$this->errCode]", 'ERR'); + return false; + } + $authorization_info['func_info'] = $this->_parseFuncInfo($authorization_info['func_info']); + return $authorization_info; + } + + /** + * 解析授权信息,返回以逗号分割的数据 + * @param array $func_info + * @return string + */ + private function _parseFuncInfo($func_info) { + $authorization_list = array(); + foreach ($func_info as $func) { + foreach ($func as $f) { + $authorization_list[] = $f['id']; + } + } + return join($authorization_list, ','); + } + + /** + * 获取授权方的账户信息 + * @param string $authorizer_appid + * @return bool + */ + public function getWechatInfo($authorizer_appid) { + empty($this->component_access_token) && $this->getComponentAccessToken(); + $data = array(); + $data['component_access_token'] = $this->component_access_token; + $data['component_appid'] = $this->component_appid; + $data['authorizer_appid'] = $authorizer_appid; + $url = self::URL_PREFIX . self::GET_AUTHORIZER_INFO_URL . "?component_access_token={$this->component_access_token}"; + $result = Tools::httpPost($url, Tools::json_encode($data)); + $authorizer_info = $this->_decode($result, 'authorizer_info'); + if (empty($authorizer_info)) { + Tools::log("Get WechatInfo Faild. {$this->errMsg} [$this->errCode]", 'ERR'); + return false; + } + $author_data = array_merge($authorizer_info, $this->data['authorization_info']); + $author_data['service_type_info'] = $author_data['service_type_info']['id']; + $author_data['verify_type_info'] = $author_data['verify_type_info']['id']; + $author_data['func_info'] = $this->_parseFuncInfo($author_data['func_info']); + $author_data['business_info'] = json_encode($author_data['business_info']); + return $author_data; + } + + /** + * 获取授权方的选项设置信息 + * @param string $authorizer_appid + * @param string $option_name + * @return bool + */ + public function getAuthorizerOption($authorizer_appid, $option_name) { + empty($this->component_access_token) && $this->getComponentAccessToken(); + if (empty($this->authorizer_appid)) { + return false; + } + $data = array(); + $data['component_appid'] = $this->component_appid; + $data['authorizer_appid'] = $authorizer_appid; + $data['option_name'] = $option_name; + $url = self::URL_PREFIX . self::GET_AUTHORIZER_OPTION_URL . "?component_access_token={$this->component_access_token}"; + $result = Tools::httpPost($url, Tools::json_encode($data)); + if (($result = $this->_decode($result)) === false) { + Tools::log("Get getAuthorizerOption Faild. {$this->errMsg} [$this->errCode]", 'ERR'); + } + return $result; + } + + /** + * 设置授权方的选项信息 + * @param string $authorizer_appid + * @param string $option_name + * @param string $option_value + * @return bool + */ + public function setAuthorizerOption($authorizer_appid, $option_name, $option_value) { + empty($this->component_access_token) && $this->getComponentAccessToken(); + if (empty($this->authorizer_appid)) { + return false; + } + $data = array(); + $data['component_appid'] = $this->component_appid; + $data['authorizer_appid'] = $authorizer_appid; + $data['option_name'] = $option_name; + $data['option_value'] = $option_value; + $url = self::URL_PREFIX . self::SET_AUTHORIZER_OPTION_URL . "?component_access_token={$this->component_access_token}"; + $result = Tools::httpPost($url, Tools::json_encode($data)); + if (($result = $this->_decode($result)) === false) { + Tools::log("Get setAuthorizerOption Faild. {$this->errMsg} [$this->errCode]", 'ERR'); + } + return $result; + } + + /** + * 获取授权回跳地址 + * @param string $redirect_uri + * @return bool + */ + public function getAuthRedirect($redirect_uri) { + empty($this->pre_auth_code) && $this->getPreauthCode(); + if (empty($this->pre_auth_code)) { + return false; + } + return "https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid={$this->component_appid}&pre_auth_code={$this->pre_auth_code}&redirect_uri={$redirect_uri}"; + } + + /** + * 获取预授权码 + * + * @return bool|string + */ + public function getPreauthCode() { + empty($this->component_access_token) && $this->getComponentAccessToken(); + if (empty($this->component_access_token)) { + return false; + } + $data = array(); + $data['component_appid'] = $this->component_appid; + $url = self::URL_PREFIX . self::PREAUTH_CODE_URL . "?component_access_token={$this->component_access_token}"; + $result = Tools::httpPost($url, Tools::json_encode($data)); + $this->pre_auth_code = $this->_decode($result, 'pre_auth_code'); + if (empty($this->pre_auth_code)) { + Tools::log("Get getPreauthCode Faild. {$this->errMsg} [$this->errCode]", 'ERR'); + } + return $this->pre_auth_code; + } + + /** + * oauth 授权跳转接口 + * @param string $appid + * @param string $redirect_uri + * @param string $scope snsapi_userinfo|snsapi_base + * @return string + */ + public function getOauthRedirect($appid, $redirect_uri, $scope = 'snsapi_userinfo') { + return "https://open.weixin.qq.com/connect/oauth2/authorize?appid={$appid}&redirect_uri=" . urlencode($redirect_uri) + . "&response_type=code&scope={$scope}&state={$appid}&component_appid={$this->component_appid}#wechat_redirect"; + } + + /** + * 通过code获取Access Token + * @param string $appid + * @return bool|array + */ + public function getOauthAccessToken($appid) { + $code = isset($_GET['code']) ? $_GET['code'] : ''; + if (empty($code)) { + return false; + } + empty($this->component_access_token) && $this->getComponentAccessToken(); + if (empty($this->component_access_token)) { + return false; + } + $url = "https://api.weixin.qq.com/sns/oauth2/component/access_token?" + . "appid={$appid}&code={$code}&" + . "grant_type=authorization_code&" + . "component_appid={$this->component_appid}&" + . "component_access_token={$this->component_access_token}"; + $json = $this->parseJson(Tools::httpGet($url)); + if ($json !== false) { + return $json; + } + return false; + } + + /** + * 解析JSON数据 + * @param string $result + * @return bool + */ + private function parseJson($result) { + $json = json_decode($result, true); + if (!empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return false; + } + return $json; + } + + /** + * 获取关注者详细信息 + * @param string $openid + * @param string $oauthAccessToken + * @return bool|array {subscribe,openid,nickname,sex,city,province,country,language,headimgurl,subscribe_time,[unionid]} + * 注意:unionid字段 只有在用户将公众号绑定到公众号第三方平台账号后,才会出现。建议调用前用isset()检测一下 + */ + public function getOauthUserInfo($openid, $oauthAccessToken) { + $url = "https://api.weixin.qq.com/sns/userinfo?access_token={$oauthAccessToken}&openid={$openid}&lang=zh_CN"; + return $this->parseJson(Tools::httpGet($url)); + } + + +} diff --git a/core/extend/Wechat/WechatUser.php b/core/extend/Wechat/WechatUser.php new file mode 100644 index 00000000..71a703dc --- /dev/null +++ b/core/extend/Wechat/WechatUser.php @@ -0,0 +1,578 @@ + + * @date 2016/06/28 11:20 + */ +class WechatUser extends Common { + + /** 获取粉丝列表 */ + const USER_GET_URL = '/user/get?'; + /* 获取粉丝信息 */ + const USER_INFO_URL = '/user/info?'; + /* 批量获取粉丝信息 */ + const USER_BATCH_INFO_URL = '/user/info/batchget?'; + /* 更新粉丝标注 */ + const USER_UPDATEREMARK_URL = '/user/info/updateremark?'; + + /** 创建标签 */ + const TAGS_CREATE_URL = '/tags/create?'; + /* 获取标签列表 */ + const TAGS_GET_URL = '/tags/get?'; + /* 更新标签 */ + const TAGS_UPDATE_URL = '/tags/update?'; + /* 删除标签 */ + const TAGS_DELETE_URL = '/tags/delete?'; + /* 获取标签下的粉丝列表 */ + const TAGS_GET_USER_URL = '/user/tag/get?'; + /* 批量为粉丝打标签 */ + const TAGS_MEMBER_BATCHTAGGING = '/tags/members/batchtagging?'; + /* 批量为粉丝取消标签 */ + const TAGS_MEMBER_BATCHUNTAGGING = '/tags/members/batchuntagging?'; + /* 获取粉丝的标签列表 */ + const TAGS_LIST = '/tags/getidlist?'; + + /** 获取分组列表 */ + const GROUP_GET_URL = '/groups/get?'; + /* 获取粉丝所在的分组 */ + const USER_GROUP_URL = '/groups/getid?'; + /* 创建分组 */ + const GROUP_CREATE_URL = '/groups/create?'; + /* 更新分组 */ + const GROUP_UPDATE_URL = '/groups/update?'; + /* 删除分组 */ + const GROUP_DELETE_URL = '/groups/delete?'; + /* 修改粉丝所在分组 */ + const GROUP_MEMBER_UPDATE_URL = '/groups/members/update?'; + /* 批量修改粉丝所在分组 */ + const GROUP_MEMBER_BATCHUPDATE_URL = '/groups/members/batchupdate?'; + + /** 获取黑名单列表 */ + const BACKLIST_GET_URL = '/tags/members/getblacklist?'; + /* 批量拉黑粉丝 */ + const BACKLIST_ADD_URL = '/tags/members/batchblacklist?'; + /* 批量取消拉黑粉丝 */ + const BACKLIST_DEL_URL = '/tags/members/batchunblacklist?'; + + /** + * 批量获取关注粉丝列表 + * @param string $next_openid + * @return bool|array + */ + public function getUserList($next_openid = '') { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_URL_PREFIX . self::USER_GET_URL . "access_token={$this->access_token}" . '&next_openid=' . $next_openid); + if ($result) { + $json = json_decode($result, true); + if (isset($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取关注者详细信息 + * @param string $openid + * @return bool|array {subscribe,openid,nickname,sex,city,province,country,language,headimgurl,subscribe_time,[unionid]} + * @注意:unionid字段 只有在粉丝将公众号绑定到微信开放平台账号后,才会出现。建议调用前用isset()检测一下 + */ + public function getUserInfo($openid) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_URL_PREFIX . self::USER_INFO_URL . "access_token={$this->access_token}&openid={$openid}"); + if ($result) { + $json = json_decode($result, true); + if (isset($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 批量获取用户基本信息 + * @param array $openids 用户oepnid列表(最多支持100个openid) + * @param string $lang 指定返回语言 + * @return bool|mixed + */ + public function getUserBatchInfo(array $openids, $lang = 'zh_CN') { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('user_list' => array()); + foreach (array_unique($openids) as $openid) { + $data['user_list'][] = array('openid' => $openid, 'lang' => $lang); + } + $result = Tools::httpPost(self::API_URL_PREFIX . self::USER_BATCH_INFO_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (isset($json['errcode']) && !isset($json['user_info_list'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json['user_info_list']; + } + return false; + } + + /** + * 设置粉丝备注名 + * @param string $openid + * @param string $remark 备注名 + * @return bool|array + */ + public function updateUserRemark($openid, $remark) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('openid' => $openid, 'remark' => $remark); + $result = Tools::httpPost(self::API_URL_PREFIX . self::USER_UPDATEREMARK_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取粉丝分组列表 + * @return bool|array + */ + public function getGroup() { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_URL_PREFIX . self::GROUP_GET_URL . "access_token={$this->access_token}"); + if ($result) { + $json = json_decode($result, true); + if (isset($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 删除粉丝分组 + * @param type $id + * @return bool + */ + public function delGroup($id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('group' => array('id' => $id)); + $result = Tools::httpPost(self::API_URL_PREFIX . self::GROUP_DELETE_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取粉丝所在分组 + * @param string $openid + * @return bool|int 成功则返回粉丝分组id + */ + public function getUserGroup($openid) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('openid' => $openid); + $result = Tools::httpPost(self::API_URL_PREFIX . self::USER_GROUP_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } else if (isset($json['groupid'])) { + return $json['groupid']; + } + } + return false; + } + + /** + * 新增自定分组 + * @param string $name 分组名称 + * @return bool|array + */ + public function createGroup($name) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('group' => array('name' => $name)); + $result = Tools::httpPost(self::API_URL_PREFIX . self::GROUP_CREATE_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 更改分组名称 + * @param int $groupid 分组id + * @param string $name 分组名称 + * @return bool|array + */ + public function updateGroup($groupid, $name) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('group' => array('id' => $groupid, 'name' => $name)); + $result = Tools::httpPost(self::API_URL_PREFIX . self::GROUP_UPDATE_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 移动粉丝分组 + * @param int $groupid 分组id + * @param string $openid 粉丝openid + * @return bool|array + */ + public function updateGroupMembers($groupid, $openid) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('openid' => $openid, 'to_groupid' => $groupid); + $result = Tools::httpPost(self::API_URL_PREFIX . self::GROUP_MEMBER_UPDATE_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 批量移动粉丝分组 + * @param string $groupid 分组ID + * @param string $openid_list 粉丝openid数组(一次不能超过50个) + * @return bool|array + */ + public function batchUpdateGroupMembers($groupid, $openid_list) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('openid_list' => $openid_list, 'to_groupid' => $groupid); + $result = Tools::httpPost(self::API_URL_PREFIX . self::GROUP_MEMBER_BATCHUPDATE_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 新增自定标签 + * @param string $name 标签名称 + * @return bool|array + */ + public function createTags($name) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('tag' => array('name' => $name)); + $result = Tools::httpPost(self::API_URL_PREFIX . self::TAGS_CREATE_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 更新标签 + * @param string $id 标签id + * @param string $name 标签名称 + * @return bool|array + */ + public function updateTag($id, $name) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('tag' => array('id' => $id, 'name' => $name)); + $result = Tools::httpPost(self::API_URL_PREFIX . self::TAGS_UPDATE_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取粉丝标签列表 + * @return bool|array + */ + public function getTags() { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $result = Tools::httpGet(self::API_URL_PREFIX . self::TAGS_GET_URL . "access_token={$this->access_token}"); + if ($result) { + $json = json_decode($result, true); + if (isset($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 删除粉丝标签 + * @param string $id + * @return bool + */ + public function delTag($id) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('tag' => array('id' => $id)); + $result = Tools::httpPost(self::API_URL_PREFIX . self::TAGS_DELETE_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取标签下的粉丝列表 + * @param string $tagid + * @param string $next_openid + * @return bool + */ + public function getTagUsers($tagid, $next_openid = '') { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('tagid' => $tagid, 'next_openid' => $next_openid); + $result = Tools::httpPost(self::API_URL_PREFIX . self::TAGS_GET_USER_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 批量为粉丝打标签 + * @param string $tagid 标签ID + * @param array $openid_list 粉丝openid数组,一次不能超过50个 + * @return bool|array + */ + public function batchAddUserTag($tagid, $openid_list) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('openid_list' => $openid_list, 'tagid' => $tagid); + $result = Tools::httpPost(self::API_URL_PREFIX . self::TAGS_MEMBER_BATCHTAGGING . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 批量为粉丝取消标签 + * @param string $tagid 标签ID + * @param array $openid_list 粉丝openid数组,一次不能超过50个 + * @return bool|array + */ + public function batchDeleteUserTag($tagid, $openid_list) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('openid_list' => $openid_list, 'tagid' => $tagid); + $result = Tools::httpPost(self::API_URL_PREFIX . self::TAGS_MEMBER_BATCHUNTAGGING . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 获取粉丝的标签列表 + * @param string $openid 粉丝openid + * @return bool|array + */ + public function getUserTags($openid) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('openid' => $openid); + $result = Tools::httpPost(self::API_URL_PREFIX . self::TAGS_LIST . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !isset($json['tagid_list']) || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json['tagid_list']; + } + return false; + } + + /** + * 批量获取黑名单粉丝 + * @param string $begin_openid + * @return bool|array + */ + public function getBacklist($begin_openid = '') { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = empty($begin_openid) ? array() : array('begin_openid' => $begin_openid); + $result = Tools::httpPost(self::API_URL_PREFIX . self::BACKLIST_GET_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (isset($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 批量拉黑粉丝 + * @param string $openids + * @return bool|array + */ + public function addBacklist($openids) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('openid_list' => $openids); + $result = Tools::httpPost(self::API_URL_PREFIX . self::BACKLIST_ADD_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + + /** + * 批量取消拉黑粉丝 + * @param string $openids + * @return bool|array + */ + public function delBacklist($openids) { + if (!$this->access_token && !$this->getAccessToken()) { + return false; + } + $data = array('openid_list' => $openids); + $result = Tools::httpPost(self::API_URL_PREFIX . self::BACKLIST_DEL_URL . "access_token={$this->access_token}", Tools::json_encode($data)); + if ($result) { + $json = json_decode($result, true); + if (!$json || !empty($json['errcode'])) { + $this->errCode = $json['errcode']; + $this->errMsg = $json['errmsg']; + return $this->checkRetry(__FUNCTION__, func_get_args()); + } + return $json; + } + return false; + } + +} diff --git a/core/lang/zh-cn.php b/core/lang/zh-cn.php index c6c5a50f..6e89f01a 100644 --- a/core/lang/zh-cn.php +++ b/core/lang/zh-cn.php @@ -33,7 +33,7 @@ return [ 'illegal action name' => '非法的操作名称', 'url suffix deny' => '禁止的URL后缀访问', 'Route Not Found' => '当前访问路由未定义', - 'Underfined db type' => '未定义数据库类型', + 'Undefined db type' => '未定义数据库类型', 'variable type error' => '变量类型错误', 'PSR-4 error' => 'PSR-4 规范错误', 'not support total' => '简洁模式下不能获取数据总数', diff --git a/core/library/think/App.php b/core/library/think/App.php index 6f2d7558..b7d59691 100644 --- a/core/library/think/App.php +++ b/core/library/think/App.php @@ -492,7 +492,7 @@ class App $dir = CONF_PATH . $module . 'extra'; $files = scandir($dir); foreach ($files as $file) { - if (pathinfo($file, PATHINFO_EXTENSION) === CONF_EXT) { + if ('.' . pathinfo($file, PATHINFO_EXTENSION) === CONF_EXT) { $filename = $dir . DS . $file; Config::load($filename, pathinfo($file, PATHINFO_FILENAME)); } diff --git a/core/library/think/Build.php b/core/library/think/Build.php index d08dc50b..6e055c92 100644 --- a/core/library/think/Build.php +++ b/core/library/think/Build.php @@ -116,10 +116,7 @@ class Build if ('__dir__' == $path) { // 生成子目录 foreach ($file as $dir) { - if (!is_dir($modulePath . $dir)) { - // 创建目录 - mkdir($modulePath . $dir, 0755, true); - } + self::checkDirBuild($modulePath . $dir); } } elseif ('__file__' == $path) { // 生成(空白)文件 @@ -144,10 +141,7 @@ class Build break; case 'view': // 视图 $filename = $modulePath . $path . DS . $val . '.html'; - if (!is_dir(dirname($filename))) { - // 创建目录 - mkdir(dirname($filename), 0755, true); - } + self::checkDirBuild(dirname($filename)); $content = ''; break; default: @@ -177,9 +171,7 @@ class Build if (!is_file($filename)) { $content = file_get_contents(THINK_PATH . 'tpl' . DS . 'default_index.tpl'); $content = str_replace(['{$app}', '{$module}', '{layer}', '{$suffix}'], [$namespace, $module ? $module . '\\' : '', 'controller', $suffix ? 'Controller' : ''], $content); - if (!is_dir(dirname($filename))) { - mkdir(dirname($filename), 0755, true); - } + self::checkDirBuild(dirname($filename)); file_put_contents($filename, $content); } } @@ -194,9 +186,7 @@ class Build { $filename = CONF_PATH . ($module ? $module . DS : '') . 'config.php'; - if (!is_dir(dirname($filename))) { - mkdir(dirname($filename, 0755, true)); - } + self::checkDirBuild(dirname($filename)); if (!is_file($filename)) { file_put_contents($filename, " +// +---------------------------------------------------------------------- + +namespace think; + +use think\console\Command; +use think\console\command\Help as HelpCommand; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; +use think\console\output\driver\Buffer; + +class Console +{ + + private $name; + private $version; + + /** @var Command[] */ + private $commands = []; + + private $wantHelps = false; + + private $catchExceptions = true; + private $autoExit = true; + private $definition; + private $defaultCommand; + + private static $defaultCommands = [ + "think\\console\\command\\Help", + "think\\console\\command\\Lists", + "think\\console\\command\\Build", + "think\\console\\command\\Clear", + "think\\console\\command\\make\\Controller", + "think\\console\\command\\make\\Model", + "think\\console\\command\\optimize\\Autoload", + "think\\console\\command\\optimize\\Config", + "think\\console\\command\\optimize\\Route", + "think\\console\\command\\optimize\\Schema", + ]; + + public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') + { + $this->name = $name; + $this->version = $version; + + $this->defaultCommand = 'list'; + $this->definition = $this->getDefaultInputDefinition(); + + foreach ($this->getDefaultCommands() as $command) { + $this->add($command); + } + } + + public static function init($run = true) + { + static $console; + if (!$console) { + // 实例化console + $console = new self('Think Console', '0.1'); + // 读取指令集 + if (is_file(CONF_PATH . 'command' . EXT)) { + $commands = include CONF_PATH . 'command' . EXT; + if (is_array($commands)) { + foreach ($commands as $command) { + if (class_exists($command) && is_subclass_of($command, "\\think\\console\\Command")) { + // 注册指令 + $console->add(new $command()); + } + } + } + } + } + if ($run) { + // 运行 + return $console->run(); + } else { + return $console; + } + } + + /** + * @param $command + * @param array $parameters + * @param string $driver + * @return Output|Buffer + */ + public static function call($command, array $parameters = [], $driver = 'buffer') + { + $console = self::init(false); + + array_unshift($parameters, $command); + + $input = new Input($parameters); + $output = new Output($driver); + + $console->setCatchExceptions(false); + $console->find($command)->run($input, $output); + + return $output; + } + + /** + * 执行当前的指令 + * @return int + * @throws \Exception + * @api + */ + public function run() + { + $input = new Input(); + $output = new Output(); + + $this->configureIO($input, $output); + + try { + $exitCode = $this->doRun($input, $output); + } catch (\Exception $e) { + if (!$this->catchExceptions) { + throw $e; + } + + $output->renderException($e); + + $exitCode = $e->getCode(); + if (is_numeric($exitCode)) { + $exitCode = (int) $exitCode; + if (0 === $exitCode) { + $exitCode = 1; + } + } else { + $exitCode = 1; + } + } + + if ($this->autoExit) { + if ($exitCode > 255) { + $exitCode = 255; + } + + exit($exitCode); + } + + return $exitCode; + } + + /** + * 执行指令 + * @param Input $input + * @param Output $output + * @return int + */ + public function doRun(Input $input, Output $output) + { + if (true === $input->hasParameterOption(['--version', '-V'])) { + $output->writeln($this->getLongVersion()); + + return 0; + } + + $name = $this->getCommandName($input); + + if (true === $input->hasParameterOption(['--help', '-h'])) { + if (!$name) { + $name = 'help'; + $input = new Input(['help']); + } else { + $this->wantHelps = true; + } + } + + if (!$name) { + $name = $this->defaultCommand; + $input = new Input([$this->defaultCommand]); + } + + $command = $this->find($name); + + $exitCode = $this->doRunCommand($command, $input, $output); + + return $exitCode; + } + + /** + * 设置输入参数定义 + * @param InputDefinition $definition + */ + public function setDefinition(InputDefinition $definition) + { + $this->definition = $definition; + } + + /** + * 获取输入参数定义 + * @return InputDefinition The InputDefinition instance + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Gets the help message. + * @return string A help message. + */ + public function getHelp() + { + return $this->getLongVersion(); + } + + /** + * 是否捕获异常 + * @param bool $boolean + * @api + */ + public function setCatchExceptions($boolean) + { + $this->catchExceptions = (bool) $boolean; + } + + /** + * 是否自动退出 + * @param bool $boolean + * @api + */ + public function setAutoExit($boolean) + { + $this->autoExit = (bool) $boolean; + } + + /** + * 获取名称 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 设置名称 + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * 获取版本 + * @return string + * @api + */ + public function getVersion() + { + return $this->version; + } + + /** + * 设置版本 + * @param string $version + */ + public function setVersion($version) + { + $this->version = $version; + } + + /** + * 获取完整的版本号 + * @return string + */ + public function getLongVersion() + { + if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) { + return sprintf('%s version %s', $this->getName(), $this->getVersion()); + } + + return 'Console Tool'; + } + + /** + * 注册一个指令 + * @param string $name + * @return Command + */ + public function register($name) + { + return $this->add(new Command($name)); + } + + /** + * 添加指令 + * @param Command[] $commands + */ + public function addCommands(array $commands) + { + foreach ($commands as $command) { + $this->add($command); + } + } + + /** + * 添加一个指令 + * @param Command $command + * @return Command + */ + public function add(Command $command) + { + $command->setConsole($this); + + if (!$command->isEnabled()) { + $command->setConsole(null); + return; + } + + if (null === $command->getDefinition()) { + throw new \LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command))); + } + + $this->commands[$command->getName()] = $command; + + foreach ($command->getAliases() as $alias) { + $this->commands[$alias] = $command; + } + + return $command; + } + + /** + * 获取指令 + * @param string $name 指令名称 + * @return Command + * @throws \InvalidArgumentException + */ + public function get($name) + { + if (!isset($this->commands[$name])) { + throw new \InvalidArgumentException(sprintf('The command "%s" does not exist.', $name)); + } + + $command = $this->commands[$name]; + + if ($this->wantHelps) { + $this->wantHelps = false; + + /** @var HelpCommand $helpCommand */ + $helpCommand = $this->get('help'); + $helpCommand->setCommand($command); + + return $helpCommand; + } + + return $command; + } + + /** + * 某个指令是否存在 + * @param string $name 指令名称 + * @return bool + */ + public function has($name) + { + return isset($this->commands[$name]); + } + + /** + * 获取所有的命名空间 + * @return array + */ + public function getNamespaces() + { + $namespaces = []; + foreach ($this->commands as $command) { + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName())); + + foreach ($command->getAliases() as $alias) { + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias)); + } + } + + return array_values(array_unique(array_filter($namespaces))); + } + + /** + * 查找注册命名空间中的名称或缩写。 + * @param string $namespace + * @return string + * @throws \InvalidArgumentException + */ + public function findNamespace($namespace) + { + $allNamespaces = $this->getNamespaces(); + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + return preg_quote($matches[1]) . '[^:]*'; + }, $namespace); + $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces); + + if (empty($namespaces)) { + $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); + + if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); + } + + $exact = in_array($namespace, $namespaces, true); + if (count($namespaces) > 1 && !$exact) { + throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces)))); + } + + return $exact ? $namespace : reset($namespaces); + } + + /** + * 查找指令 + * @param string $name 名称或者别名 + * @return Command + * @throws \InvalidArgumentException + */ + public function find($name) + { + $allCommands = array_keys($this->commands); + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + return preg_quote($matches[1]) . '[^:]*'; + }, $name); + $commands = preg_grep('{^' . $expr . '}', $allCommands); + + if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) { + if (false !== $pos = strrpos($name, ':')) { + $this->findNamespace(substr($name, 0, $pos)); + } + + $message = sprintf('Command "%s" is not defined.', $name); + + if ($alternatives = $this->findAlternatives($name, $allCommands)) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); + } + + if (count($commands) > 1) { + $commandList = $this->commands; + $commands = array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) { + $commandName = $commandList[$nameOrAlias]->getName(); + + return $commandName === $nameOrAlias || !in_array($commandName, $commands); + }); + } + + $exact = in_array($name, $commands, true); + if (count($commands) > 1 && !$exact) { + $suggestions = $this->getAbbreviationSuggestions(array_values($commands)); + + throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)); + } + + return $this->get($exact ? $name : reset($commands)); + } + + /** + * 获取所有的指令 + * @param string $namespace 命名空间 + * @return Command[] + * @api + */ + public function all($namespace = null) + { + if (null === $namespace) { + return $this->commands; + } + + $commands = []; + foreach ($this->commands as $name => $command) { + if ($this->extractNamespace($name, substr_count($namespace, ':') + 1) === $namespace) { + $commands[$name] = $command; + } + } + + return $commands; + } + + /** + * 获取可能的指令名 + * @param array $names + * @return array + */ + public static function getAbbreviations($names) + { + $abbrevs = []; + foreach ($names as $name) { + for ($len = strlen($name); $len > 0; --$len) { + $abbrev = substr($name, 0, $len); + $abbrevs[$abbrev][] = $name; + } + } + + return $abbrevs; + } + + /** + * 配置基于用户的参数和选项的输入和输出实例。 + * @param Input $input 输入实例 + * @param Output $output 输出实例 + */ + protected function configureIO(Input $input, Output $output) + { + if (true === $input->hasParameterOption(['--ansi'])) { + $output->setDecorated(true); + } elseif (true === $input->hasParameterOption(['--no-ansi'])) { + $output->setDecorated(false); + } + + if (true === $input->hasParameterOption(['--no-interaction', '-n'])) { + $input->setInteractive(false); + } + + if (true === $input->hasParameterOption(['--quiet', '-q'])) { + $output->setVerbosity(Output::VERBOSITY_QUIET); + } else { + if ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) { + $output->setVerbosity(Output::VERBOSITY_DEBUG); + } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) { + $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE); + } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) { + $output->setVerbosity(Output::VERBOSITY_VERBOSE); + } + } + } + + /** + * 执行指令 + * @param Command $command 指令实例 + * @param Input $input 输入实例 + * @param Output $output 输出实例 + * @return int + * @throws \Exception + */ + protected function doRunCommand(Command $command, Input $input, Output $output) + { + return $command->run($input, $output); + } + + /** + * 获取指令的基础名称 + * @param Input $input + * @return string + */ + protected function getCommandName(Input $input) + { + return $input->getFirstArgument(); + } + + /** + * 获取默认输入定义 + * @return InputDefinition + */ + protected function getDefaultInputDefinition() + { + return new InputDefinition([ + new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), + new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'), + new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'), + new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'), + new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), + new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'), + new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'), + new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), + ]); + } + + /** + * 设置默认命令 + * @return Command[] An array of default Command instances + */ + protected function getDefaultCommands() + { + $defaultCommands = []; + + foreach (self::$defaultCommands as $classname) { + if (class_exists($classname) && is_subclass_of($classname, "think\\console\\Command")) { + $defaultCommands[] = new $classname(); + } + } + + return $defaultCommands; + } + + public static function addDefaultCommands(array $classnames) + { + self::$defaultCommands = array_merge(self::$defaultCommands, $classnames); + } + + /** + * 获取可能的建议 + * @param array $abbrevs + * @return string + */ + private function getAbbreviationSuggestions($abbrevs) + { + return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : ''); + } + + /** + * 返回命名空间部分 + * @param string $name 指令 + * @param string $limit 部分的命名空间的最大数量 + * @return string + */ + public function extractNamespace($name, $limit = null) + { + $parts = explode(':', $name); + array_pop($parts); + + return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit)); + } + + /** + * 查找可替代的建议 + * @param string $name + * @param array|\Traversable $collection + * @return array + */ + private function findAlternatives($name, $collection) + { + $threshold = 1e3; + $alternatives = []; + + $collectionParts = []; + foreach ($collection as $item) { + $collectionParts[$item] = explode(':', $item); + } + + foreach (explode(':', $name) as $i => $subname) { + foreach ($collectionParts as $collectionName => $parts) { + $exists = isset($alternatives[$collectionName]); + if (!isset($parts[$i]) && $exists) { + $alternatives[$collectionName] += $threshold; + continue; + } elseif (!isset($parts[$i])) { + continue; + } + + $lev = levenshtein($subname, $parts[$i]); + if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) { + $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; + } elseif ($exists) { + $alternatives[$collectionName] += $threshold; + } + } + } + + foreach ($collection as $item) { + $lev = levenshtein($name, $item); + if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { + $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; + } + } + + $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { + return $lev < 2 * $threshold; + }); + asort($alternatives); + + return array_keys($alternatives); + } + + /** + * 设置默认的指令 + * @param string $commandName The Command name + */ + public function setDefaultCommand($commandName) + { + $this->defaultCommand = $commandName; + } + + /** + * 返回所有的命名空间 + * @param string $name + * @return array + */ + private function extractAllNamespaces($name) + { + $parts = explode(':', $name, -1); + $namespaces = []; + + foreach ($parts as $part) { + if (count($namespaces)) { + $namespaces[] = end($namespaces) . ':' . $part; + } else { + $namespaces[] = $part; + } + } + + return $namespaces; + } + +} diff --git a/core/library/think/Db.php b/core/library/think/Db.php index 3e613f7a..e239fd4e 100644 --- a/core/library/think/Db.php +++ b/core/library/think/Db.php @@ -74,7 +74,7 @@ class Db // 解析连接参数 支持数组和字符串 $options = self::parseConfig($config); if (empty($options['type'])) { - throw new \InvalidArgumentException('Underfined db type'); + throw new \InvalidArgumentException('Undefined db type'); } $class = false !== strpos($options['type'], '\\') ? $options['type'] : '\\think\\db\\connector\\' . ucwords($options['type']); // 记录初始化信息 diff --git a/core/library/think/Log.php b/core/library/think/Log.php index 5a658880..a20ab262 100644 --- a/core/library/think/Log.php +++ b/core/library/think/Log.php @@ -158,7 +158,7 @@ class Log if ($result) { self::$log = []; } - + Hook::listen('log_write_done', $log); return $result; } return true; diff --git a/core/library/think/Model.php b/core/library/think/Model.php index 6fc8cabe..602f77ab 100644 --- a/core/library/think/Model.php +++ b/core/library/think/Model.php @@ -838,6 +838,17 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return json_encode($this->toArray(), $options); } + /** + * 移除当前模型的关联属性 + * @access public + * @return $this + */ + public function removeRelation() + { + $this->relation = []; + return $this; + } + /** * 转换当前模型数据集为数据集对象 * @access public @@ -964,9 +975,6 @@ abstract class Model implements \JsonSerializable, \ArrayAccess } $pk = $this->getPk(); if ($this->isUpdate) { - // 检测字段 - $this->checkAllowField($this->data, array_merge($this->auto, $this->update)); - // 自动更新 $this->autoCompleteData($this->update); @@ -986,7 +994,8 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return 0; } elseif ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) { // 自动写入更新时间 - $data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); + $data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); + $this->data[$this->updateTime] = $data[$this->updateTime]; } if (empty($where) && !empty($this->updateWhere)) { @@ -1008,8 +1017,15 @@ abstract class Model implements \JsonSerializable, \ArrayAccess unset($data[$pk]); } + // 检测字段 + $allowFields = $this->checkAllowField(array_merge($this->auto, $this->update)); + // 模型更新 - $result = $this->getQuery()->where($where)->update($data); + if (!empty($allowFields)) { + $result = $this->getQuery()->where($where)->strict(false)->field($allowFields)->update($data); + } else { + $result = $this->getQuery()->where($where)->update($data); + } // 关联更新 if (isset($relation)) { @@ -1020,9 +1036,6 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $this->trigger('after_update', $this); } else { - // 检测字段 - $this->checkAllowField($this->data, array_merge($this->auto, $this->insert)); - // 自动写入 $this->autoCompleteData($this->insert); @@ -1040,7 +1053,13 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return false; } - $result = $this->getQuery()->insert($this->data); + // 检测字段 + $allowFields = $this->checkAllowField(array_merge($this->auto, $this->insert)); + if (!empty($allowFields)) { + $result = $this->getQuery()->strict(false)->field($allowFields)->insert($this->data); + } else { + $result = $this->getQuery()->insert($this->data); + } // 获取自动增长主键 if ($result && is_string($pk) && (!isset($this->data[$pk]) || '' == $this->data[$pk])) { @@ -1073,22 +1092,22 @@ abstract class Model implements \JsonSerializable, \ArrayAccess return $result; } - protected function checkAllowField(&$data, $auto = []) + protected function checkAllowField($auto = []) { if (!empty($this->field)) { - if (true === $this->field) { + if (!empty($this->origin)) { + $this->field = array_keys($this->origin); + $field = $this->field; + } elseif (true === $this->field) { $this->field = $this->getQuery()->getTableInfo('', 'fields'); $field = $this->field; } else { $field = array_merge($this->field, $auto); } - - foreach ($data as $key => $val) { - if (!in_array($key, $field)) { - unset($data[$key]); - } - } + } else { + $field = []; } + return $field; } protected function autoRelationUpdate($relation) @@ -1627,6 +1646,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess $model = new static(); $query = $model->db(); $params = func_get_args(); + array_shift($params); array_unshift($params, $query); if ($name instanceof \Closure) { call_user_func_array($name, $params); @@ -1927,7 +1947,7 @@ abstract class Model implements \JsonSerializable, \ArrayAccess // 记录当前关联信息 $model = $this->parseModel($model); $name = Loader::parseName(basename(str_replace('\\', '/', $model))); - $table = $table ?: $this->getQuery()->getTable(Loader::parseName($this->name) . '_' . $name); + $table = $table ?: Loader::parseName($this->name) . '_' . $name; $foreignKey = $foreignKey ?: $name . '_id'; $localKey = $localKey ?: $this->getForeignKey($this->name); return new BelongsToMany($this, $model, $table, $foreignKey, $localKey); diff --git a/core/library/think/Request.php b/core/library/think/Request.php index e3fa3a4f..7e9949a8 100644 --- a/core/library/think/Request.php +++ b/core/library/think/Request.php @@ -1215,6 +1215,8 @@ class Request return true; } elseif (isset($server['HTTP_X_FORWARDED_PROTO']) && 'https' == $server['HTTP_X_FORWARDED_PROTO']) { return true; + } elseif (Config::get('https_agent_name') && isset($server[Config::get('https_agent_name')])) { + return true; } return false; } @@ -1542,7 +1544,7 @@ class Request $key = call_user_func_array($key, [$this]); } elseif (true === $key) { foreach ($except as $rule) { - if (0 === strpos($this->url(), $rule)) { + if (0 === stripos($this->url(), $rule)) { return; } } diff --git a/core/library/think/Response.php b/core/library/think/Response.php index 96737dcd..7ae9fed7 100644 --- a/core/library/think/Response.php +++ b/core/library/think/Response.php @@ -89,6 +89,9 @@ class Response */ public function send() { + // 监听response_send + Hook::listen('response_send', $this); + // 处理输出数据 $data = $this->getContent(); diff --git a/core/library/think/Route.php b/core/library/think/Route.php index a5b19327..59b15432 100644 --- a/core/library/think/Route.php +++ b/core/library/think/Route.php @@ -1159,7 +1159,7 @@ class Route private static function checkRule($rule, $route, $url, $pattern, $option, $depr) { // 检查完整规则定义 - if (isset($pattern['__url__']) && !preg_match('/^' . $pattern['__url__'] . '/', str_replace('|', $depr, $url))) { + if (isset($pattern['__url__']) && !preg_match(0 === strpos($pattern['__url__'], '/') ? $pattern['__url__'] : '/^' . $pattern['__url__'] . '/', str_replace('|', $depr, $url))) { return false; } // 检查路由的参数分隔符 @@ -1349,7 +1349,7 @@ class Route if (false === $result) { return false; } - } elseif (!preg_match('/^' . $pattern[$name] . '$/', $m1[$key])) { + } elseif (!preg_match(0 === strpos($pattern[$name], '/') ? $pattern[$name] : '/^' . $pattern[$name] . '$/', $m1[$key])) { return false; } } @@ -1449,6 +1449,10 @@ class Route $request->bind($bind); } + if (!empty($option['response'])) { + Hook::add('response_send', $option['response']); + } + // 解析额外参数 self::parseUrlParams(empty($paths) ? '' : implode('|', $paths), $matches); // 记录匹配的路由信息 diff --git a/core/library/think/cache/Driver.php b/core/library/think/cache/Driver.php index 688507a8..ab48bdd6 100644 --- a/core/library/think/cache/Driver.php +++ b/core/library/think/cache/Driver.php @@ -170,8 +170,9 @@ abstract class Driver $key = 'tag_' . md5($this->tag); $this->tag = null; if ($this->has($key)) { - $value = $this->get($key); - $value .= ',' . $name; + $value = explode(',', $this->get($key)); + $value[] = $name; + $value = implode(',', array_unique($value)); } else { $value = $name; } diff --git a/core/library/think/console/Command.php b/core/library/think/console/Command.php new file mode 100644 index 00000000..d0caad2f --- /dev/null +++ b/core/library/think/console/Command.php @@ -0,0 +1,470 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use think\Console; +use think\console\input\Argument; +use think\console\input\Definition; +use think\console\input\Option; + +class Command +{ + + /** @var Console */ + private $console; + private $name; + private $aliases = []; + private $definition; + private $help; + private $description; + private $ignoreValidationErrors = false; + private $consoleDefinitionMerged = false; + private $consoleDefinitionMergedWithArgs = false; + private $code; + private $synopsis = []; + private $usages = []; + + /** @var Input */ + protected $input; + + /** @var Output */ + protected $output; + + /** + * 构造方法 + * @param string|null $name 命令名称,如果没有设置则比如在 configure() 里设置 + * @throws \LogicException + * @api + */ + public function __construct($name = null) + { + $this->definition = new Definition(); + + if (null !== $name) { + $this->setName($name); + } + + $this->configure(); + + if (!$this->name) { + throw new \LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this))); + } + } + + /** + * 忽略验证错误 + */ + public function ignoreValidationErrors() + { + $this->ignoreValidationErrors = true; + } + + /** + * 设置控制台 + * @param Console $console + */ + public function setConsole(Console $console = null) + { + $this->console = $console; + } + + /** + * 获取控制台 + * @return Console + * @api + */ + public function getConsole() + { + return $this->console; + } + + /** + * 是否有效 + * @return bool + */ + public function isEnabled() + { + return true; + } + + /** + * 配置指令 + */ + protected function configure() + { + } + + /** + * 执行指令 + * @param Input $input + * @param Output $output + * @return null|int + * @throws \LogicException + * @see setCode() + */ + protected function execute(Input $input, Output $output) + { + throw new \LogicException('You must override the execute() method in the concrete command class.'); + } + + /** + * 用户验证 + * @param Input $input + * @param Output $output + */ + protected function interact(Input $input, Output $output) + { + } + + /** + * 初始化 + * @param Input $input An InputInterface instance + * @param Output $output An OutputInterface instance + */ + protected function initialize(Input $input, Output $output) + { + } + + /** + * 执行 + * @param Input $input + * @param Output $output + * @return int + * @throws \Exception + * @see setCode() + * @see execute() + */ + public function run(Input $input, Output $output) + { + $this->input = $input; + $this->output = $output; + + $this->getSynopsis(true); + $this->getSynopsis(false); + + $this->mergeConsoleDefinition(); + + try { + $input->bind($this->definition); + } catch (\Exception $e) { + if (!$this->ignoreValidationErrors) { + throw $e; + } + } + + $this->initialize($input, $output); + + if ($input->isInteractive()) { + $this->interact($input, $output); + } + + $input->validate(); + + if ($this->code) { + $statusCode = call_user_func($this->code, $input, $output); + } else { + $statusCode = $this->execute($input, $output); + } + + return is_numeric($statusCode) ? (int) $statusCode : 0; + } + + /** + * 设置执行代码 + * @param callable $code callable(InputInterface $input, OutputInterface $output) + * @return Command + * @throws \InvalidArgumentException + * @see execute() + */ + public function setCode(callable $code) + { + if (!is_callable($code)) { + throw new \InvalidArgumentException('Invalid callable provided to Command::setCode.'); + } + + if (PHP_VERSION_ID >= 50400 && $code instanceof \Closure) { + $r = new \ReflectionFunction($code); + if (null === $r->getClosureThis()) { + $code = \Closure::bind($code, $this); + } + } + + $this->code = $code; + + return $this; + } + + /** + * 合并参数定义 + * @param bool $mergeArgs + */ + public function mergeConsoleDefinition($mergeArgs = true) + { + if (null === $this->console + || (true === $this->consoleDefinitionMerged + && ($this->consoleDefinitionMergedWithArgs || !$mergeArgs)) + ) { + return; + } + + if ($mergeArgs) { + $currentArguments = $this->definition->getArguments(); + $this->definition->setArguments($this->console->getDefinition()->getArguments()); + $this->definition->addArguments($currentArguments); + } + + $this->definition->addOptions($this->console->getDefinition()->getOptions()); + + $this->consoleDefinitionMerged = true; + if ($mergeArgs) { + $this->consoleDefinitionMergedWithArgs = true; + } + } + + /** + * 设置参数定义 + * @param array|Definition $definition + * @return Command + * @api + */ + public function setDefinition($definition) + { + if ($definition instanceof Definition) { + $this->definition = $definition; + } else { + $this->definition->setDefinition($definition); + } + + $this->consoleDefinitionMerged = false; + + return $this; + } + + /** + * 获取参数定义 + * @return Definition + * @api + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * 获取当前指令的参数定义 + * @return Definition + */ + public function getNativeDefinition() + { + return $this->getDefinition(); + } + + /** + * 添加参数 + * @param string $name 名称 + * @param int $mode 类型 + * @param string $description 描述 + * @param mixed $default 默认值 + * @return Command + */ + public function addArgument($name, $mode = null, $description = '', $default = null) + { + $this->definition->addArgument(new Argument($name, $mode, $description, $default)); + + return $this; + } + + /** + * 添加选项 + * @param string $name 选项名称 + * @param string $shortcut 别名 + * @param int $mode 类型 + * @param string $description 描述 + * @param mixed $default 默认值 + * @return Command + */ + public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + $this->definition->addOption(new Option($name, $shortcut, $mode, $description, $default)); + + return $this; + } + + /** + * 设置指令名称 + * @param string $name + * @return Command + * @throws \InvalidArgumentException + */ + public function setName($name) + { + $this->validateName($name); + + $this->name = $name; + + return $this; + } + + /** + * 获取指令名称 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 设置描述 + * @param string $description + * @return Command + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * 获取描述 + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * 设置帮助信息 + * @param string $help + * @return Command + */ + public function setHelp($help) + { + $this->help = $help; + + return $this; + } + + /** + * 获取帮助信息 + * @return string + */ + public function getHelp() + { + return $this->help; + } + + /** + * 描述信息 + * @return string + */ + public function getProcessedHelp() + { + $name = $this->name; + + $placeholders = [ + '%command.name%', + '%command.full_name%', + ]; + $replacements = [ + $name, + $_SERVER['PHP_SELF'] . ' ' . $name, + ]; + + return str_replace($placeholders, $replacements, $this->getHelp()); + } + + /** + * 设置别名 + * @param string[] $aliases + * @return Command + * @throws \InvalidArgumentException + */ + public function setAliases($aliases) + { + if (!is_array($aliases) && !$aliases instanceof \Traversable) { + throw new \InvalidArgumentException('$aliases must be an array or an instance of \Traversable'); + } + + foreach ($aliases as $alias) { + $this->validateName($alias); + } + + $this->aliases = $aliases; + + return $this; + } + + /** + * 获取别名 + * @return array + */ + public function getAliases() + { + return $this->aliases; + } + + /** + * 获取简介 + * @param bool $short 是否简单的 + * @return string + */ + public function getSynopsis($short = false) + { + $key = $short ? 'short' : 'long'; + + if (!isset($this->synopsis[$key])) { + $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short))); + } + + return $this->synopsis[$key]; + } + + /** + * 添加用法介绍 + * @param string $usage + * @return $this + */ + public function addUsage($usage) + { + if (0 !== strpos($usage, $this->name)) { + $usage = sprintf('%s %s', $this->name, $usage); + } + + $this->usages[] = $usage; + + return $this; + } + + /** + * 获取用法介绍 + * @return array + */ + public function getUsages() + { + return $this->usages; + } + + /** + * 验证指令名称 + * @param string $name + * @throws \InvalidArgumentException + */ + private function validateName($name) + { + if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) { + throw new \InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); + } + } +} diff --git a/core/library/think/console/Input.php b/core/library/think/console/Input.php new file mode 100644 index 00000000..2482dfdc --- /dev/null +++ b/core/library/think/console/Input.php @@ -0,0 +1,464 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use think\console\input\Argument; +use think\console\input\Definition; +use think\console\input\Option; + +class Input +{ + + /** + * @var Definition + */ + protected $definition; + + /** + * @var Option[] + */ + protected $options = []; + + /** + * @var Argument[] + */ + protected $arguments = []; + + protected $interactive = true; + + private $tokens; + private $parsed; + + public function __construct($argv = null) + { + if (null === $argv) { + $argv = $_SERVER['argv']; + // 去除命令名 + array_shift($argv); + } + + $this->tokens = $argv; + + $this->definition = new Definition(); + } + + protected function setTokens(array $tokens) + { + $this->tokens = $tokens; + } + + /** + * 绑定实例 + * @param Definition $definition A InputDefinition instance + */ + public function bind(Definition $definition) + { + $this->arguments = []; + $this->options = []; + $this->definition = $definition; + + $this->parse(); + } + + /** + * 解析参数 + */ + protected function parse() + { + $parseOptions = true; + $this->parsed = $this->tokens; + while (null !== $token = array_shift($this->parsed)) { + if ($parseOptions && '' == $token) { + $this->parseArgument($token); + } elseif ($parseOptions && '--' == $token) { + $parseOptions = false; + } elseif ($parseOptions && 0 === strpos($token, '--')) { + $this->parseLongOption($token); + } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { + $this->parseShortOption($token); + } else { + $this->parseArgument($token); + } + } + } + + /** + * 解析短选项 + * @param string $token 当前的指令. + */ + private function parseShortOption($token) + { + $name = substr($token, 1); + + if (strlen($name) > 1) { + if ($this->definition->hasShortcut($name[0]) + && $this->definition->getOptionForShortcut($name[0])->acceptValue() + ) { + $this->addShortOption($name[0], substr($name, 1)); + } else { + $this->parseShortOptionSet($name); + } + } else { + $this->addShortOption($name, null); + } + } + + /** + * 解析短选项 + * @param string $name 当前指令 + * @throws \RuntimeException + */ + private function parseShortOptionSet($name) + { + $len = strlen($name); + for ($i = 0; $i < $len; ++$i) { + if (!$this->definition->hasShortcut($name[$i])) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i])); + } + + $option = $this->definition->getOptionForShortcut($name[$i]); + if ($option->acceptValue()) { + $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); + + break; + } else { + $this->addLongOption($option->getName(), null); + } + } + } + + /** + * 解析完整选项 + * @param string $token 当前指令 + */ + private function parseLongOption($token) + { + $name = substr($token, 2); + + if (false !== $pos = strpos($name, '=')) { + $this->addLongOption(substr($name, 0, $pos), substr($name, $pos + 1)); + } else { + $this->addLongOption($name, null); + } + } + + /** + * 解析参数 + * @param string $token 当前指令 + * @throws \RuntimeException + */ + private function parseArgument($token) + { + $c = count($this->arguments); + + if ($this->definition->hasArgument($c)) { + $arg = $this->definition->getArgument($c); + + $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; + + } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { + $arg = $this->definition->getArgument($c - 1); + + $this->arguments[$arg->getName()][] = $token; + } else { + throw new \RuntimeException('Too many arguments.'); + } + } + + /** + * 添加一个短选项的值 + * @param string $shortcut 短名称 + * @param mixed $value 值 + * @throws \RuntimeException + */ + private function addShortOption($shortcut, $value) + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + /** + * 添加一个完整选项的值 + * @param string $name 选项名 + * @param mixed $value 值 + * @throws \RuntimeException + */ + private function addLongOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \RuntimeException(sprintf('The "--%s" option does not exist.', $name)); + } + + $option = $this->definition->getOption($name); + + if (false === $value) { + $value = null; + } + + if (null !== $value && !$option->acceptValue()) { + throw new \RuntimeException(sprintf('The "--%s" option does not accept a value.', $name, $value)); + } + + if (null === $value && $option->acceptValue() && count($this->parsed)) { + $next = array_shift($this->parsed); + if (isset($next[0]) && '-' !== $next[0]) { + $value = $next; + } elseif (empty($next)) { + $value = ''; + } else { + array_unshift($this->parsed, $next); + } + } + + if (null === $value) { + if ($option->isValueRequired()) { + throw new \RuntimeException(sprintf('The "--%s" option requires a value.', $name)); + } + + if (!$option->isArray()) { + $value = $option->isValueOptional() ? $option->getDefault() : true; + } + } + + if ($option->isArray()) { + $this->options[$name][] = $value; + } else { + $this->options[$name] = $value; + } + } + + /** + * 获取第一个参数 + * @return string|null + */ + public function getFirstArgument() + { + foreach ($this->tokens as $token) { + if ($token && '-' === $token[0]) { + continue; + } + + return $token; + } + return; + } + + /** + * 检查原始参数是否包含某个值 + * @param string|array $values 需要检查的值 + * @return bool + */ + public function hasParameterOption($values) + { + $values = (array) $values; + + foreach ($this->tokens as $token) { + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + return true; + } + } + } + + return false; + } + + /** + * 获取原始选项的值 + * @param string|array $values 需要检查的值 + * @param mixed $default 默认值 + * @return mixed The option value + */ + public function getParameterOption($values, $default = false) + { + $values = (array) $values; + $tokens = $this->tokens; + + while (0 < count($tokens)) { + $token = array_shift($tokens); + + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + if (false !== $pos = strpos($token, '=')) { + return substr($token, $pos + 1); + } + + return array_shift($tokens); + } + } + } + + return $default; + } + + /** + * 验证输入 + * @throws \RuntimeException + */ + public function validate() + { + if (count($this->arguments) < $this->definition->getArgumentRequiredCount()) { + throw new \RuntimeException('Not enough arguments.'); + } + } + + /** + * 检查输入是否是交互的 + * @return bool + */ + public function isInteractive() + { + return $this->interactive; + } + + /** + * 设置输入的交互 + * @param bool + */ + public function setInteractive($interactive) + { + $this->interactive = (bool) $interactive; + } + + /** + * 获取所有的参数 + * @return Argument[] + */ + public function getArguments() + { + return array_merge($this->definition->getArgumentDefaults(), $this->arguments); + } + + /** + * 根据名称获取参数 + * @param string $name 参数名 + * @return mixed + * @throws \InvalidArgumentException + */ + public function getArgument($name) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + return isset($this->arguments[$name]) ? $this->arguments[$name] : $this->definition->getArgument($name) + ->getDefault(); + } + + /** + * 设置参数的值 + * @param string $name 参数名 + * @param string $value 值 + * @throws \InvalidArgumentException + */ + public function setArgument($name, $value) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } + + /** + * 检查是否存在某个参数 + * @param string|int $name 参数名或位置 + * @return bool + */ + public function hasArgument($name) + { + return $this->definition->hasArgument($name); + } + + /** + * 获取所有的选项 + * @return Option[] + */ + public function getOptions() + { + return array_merge($this->definition->getOptionDefaults(), $this->options); + } + + /** + * 获取选项值 + * @param string $name 选项名称 + * @return mixed + * @throws \InvalidArgumentException + */ + public function getOption($name) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + return isset($this->options[$name]) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); + } + + /** + * 设置选项值 + * @param string $name 选项名 + * @param string|bool $value 值 + * @throws \InvalidArgumentException + */ + public function setOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + $this->options[$name] = $value; + } + + /** + * 是否有某个选项 + * @param string $name 选项名 + * @return bool + */ + public function hasOption($name) + { + return $this->definition->hasOption($name) && isset($this->options[$name]); + } + + /** + * 转义指令 + * @param string $token + * @return string + */ + public function escapeToken($token) + { + return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); + } + + /** + * 返回传递给命令的参数的字符串 + * @return string + */ + public function __toString() + { + $tokens = array_map(function ($token) { + if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { + return $match[1] . $this->escapeToken($match[2]); + } + + if ($token && '-' !== $token[0]) { + return $this->escapeToken($token); + } + + return $token; + }, $this->tokens); + + return implode(' ', $tokens); + } +} diff --git a/core/library/think/console/LICENSE b/core/library/think/console/LICENSE new file mode 100644 index 00000000..0abe056e --- /dev/null +++ b/core/library/think/console/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2016 Fabien Potencier + +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. \ No newline at end of file diff --git a/core/library/think/console/Output.php b/core/library/think/console/Output.php new file mode 100644 index 00000000..65dc9fb8 --- /dev/null +++ b/core/library/think/console/Output.php @@ -0,0 +1,222 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use Exception; +use think\console\output\Ask; +use think\console\output\Descriptor; +use think\console\output\driver\Buffer; +use think\console\output\driver\Console; +use think\console\output\driver\Nothing; +use think\console\output\Question; +use think\console\output\question\Choice; +use think\console\output\question\Confirmation; + +/** + * Class Output + * @package think\console + * + * @see \think\console\output\driver\Console::setDecorated + * @method void setDecorated($decorated) + * + * @see \think\console\output\driver\Buffer::fetch + * @method string fetch() + * + * @method void info($message) + * @method void error($message) + * @method void comment($message) + * @method void warning($message) + * @method void highlight($message) + * @method void question($message) + */ +class Output +{ + const VERBOSITY_QUIET = 0; + const VERBOSITY_NORMAL = 1; + const VERBOSITY_VERBOSE = 2; + const VERBOSITY_VERY_VERBOSE = 3; + const VERBOSITY_DEBUG = 4; + + const OUTPUT_NORMAL = 0; + const OUTPUT_RAW = 1; + const OUTPUT_PLAIN = 2; + + private $verbosity = self::VERBOSITY_NORMAL; + + /** @var Buffer|Console|Nothing */ + private $handle = null; + + protected $styles = [ + 'info', + 'error', + 'comment', + 'question', + 'highlight', + 'warning' + ]; + + public function __construct($driver = 'console') + { + $class = '\\think\\console\\output\\driver\\' . ucwords($driver); + + $this->handle = new $class($this); + } + + public function ask(Input $input, $question, $default = null, $validator = null) + { + $question = new Question($question, $default); + $question->setValidator($validator); + + return $this->askQuestion($input, $question); + } + + public function askHidden(Input $input, $question, $validator = null) + { + $question = new Question($question); + + $question->setHidden(true); + $question->setValidator($validator); + + return $this->askQuestion($input, $question); + } + + public function confirm(Input $input, $question, $default = true) + { + return $this->askQuestion($input, new Confirmation($question, $default)); + } + + /** + * {@inheritdoc} + */ + public function choice(Input $input, $question, array $choices, $default = null) + { + if (null !== $default) { + $values = array_flip($choices); + $default = $values[$default]; + } + + return $this->askQuestion($input, new Choice($question, $choices, $default)); + } + + protected function askQuestion(Input $input, Question $question) + { + $ask = new Ask($input, $this, $question); + $answer = $ask->run(); + + if ($input->isInteractive()) { + $this->newLine(); + } + + return $answer; + } + + protected function block($style, $message) + { + $this->writeln("<{$style}>{$message}"); + } + + /** + * 输出空行 + * @param int $count + */ + public function newLine($count = 1) + { + $this->write(str_repeat(PHP_EOL, $count)); + } + + /** + * 输出信息并换行 + * @param string $messages + * @param int $type + */ + public function writeln($messages, $type = self::OUTPUT_NORMAL) + { + $this->write($messages, true, $type); + } + + /** + * 输出信息 + * @param string $messages + * @param bool $newline + * @param int $type + */ + public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) + { + $this->handle->write($messages, $newline, $type); + } + + public function renderException(\Exception $e) + { + $this->handle->renderException($e); + } + + /** + * {@inheritdoc} + */ + public function setVerbosity($level) + { + $this->verbosity = (int) $level; + } + + /** + * {@inheritdoc} + */ + public function getVerbosity() + { + return $this->verbosity; + } + + public function isQuiet() + { + return self::VERBOSITY_QUIET === $this->verbosity; + } + + public function isVerbose() + { + return self::VERBOSITY_VERBOSE <= $this->verbosity; + } + + public function isVeryVerbose() + { + return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; + } + + public function isDebug() + { + return self::VERBOSITY_DEBUG <= $this->verbosity; + } + + public function describe($object, array $options = []) + { + $descriptor = new Descriptor(); + $options = array_merge([ + 'raw_text' => false, + ], $options); + + $descriptor->describe($this, $object, $options); + } + + public function __call($method, $args) + { + if (in_array($method, $this->styles)) { + array_unshift($args, $method); + return call_user_func_array([$this, 'block'], $args); + } + + if ($this->handle && method_exists($this->handle, $method)) { + return call_user_func_array([$this->handle, $method], $args); + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } + +} diff --git a/core/library/think/console/bin/README.md b/core/library/think/console/bin/README.md new file mode 100644 index 00000000..9acc52fb --- /dev/null +++ b/core/library/think/console/bin/README.md @@ -0,0 +1 @@ +console 工具使用 hiddeninput.exe 在 windows 上隐藏密码输入,该二进制文件由第三方提供,相关源码和其他细节可以在 [Hidden Input](https://github.com/Seldaek/hidden-input) 找到。 diff --git a/core/library/think/console/bin/hiddeninput.exe b/core/library/think/console/bin/hiddeninput.exe new file mode 100644 index 00000000..c8cf65e8 Binary files /dev/null and b/core/library/think/console/bin/hiddeninput.exe differ diff --git a/core/library/think/console/command/Build.php b/core/library/think/console/command/Build.php new file mode 100644 index 00000000..39806c3f --- /dev/null +++ b/core/library/think/console/command/Build.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; + +class Build extends Command +{ + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName('build') + ->setDefinition([ + new Option('config', null, Option::VALUE_OPTIONAL, "build.php path"), + new Option('module', null, Option::VALUE_OPTIONAL, "module name"), + ]) + ->setDescription('Build Application Dirs'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->hasOption('module')) { + \think\Build::module($input->getOption('module')); + $output->writeln("Successed"); + return; + } + + if ($input->hasOption('config')) { + $build = include $input->getOption('config'); + } else { + $build = include APP_PATH . 'build.php'; + } + if (empty($build)) { + $output->writeln("Build Config Is Empty"); + return; + } + \think\Build::run($build); + $output->writeln("Successed"); + + } +} diff --git a/core/library/think/console/command/Clear.php b/core/library/think/console/command/Clear.php new file mode 100644 index 00000000..41019cea --- /dev/null +++ b/core/library/think/console/command/Clear.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; + +class Clear extends Command +{ + protected function configure() + { + // 指令配置 + $this + ->setName('clear') + ->addOption('path', 'd', Option::VALUE_OPTIONAL, 'path to clear', null) + ->setDescription('Clear runtime file'); + } + + protected function execute(Input $input, Output $output) + { + $path = $input->getOption('path') ?: RUNTIME_PATH; + + if (is_dir($path)) { + $this->clearPath($path); + } + + $output->writeln("Clear Successed"); + } + + protected function clearPath($path) + { + $path = realpath($path) . DS; + $files = scandir($path); + if ($files) { + foreach ($files as $file) { + if ('.' != $file && '..' != $file && is_dir($path . $file)) { + $this->clearPath($path . $file); + } elseif ('.gitignore' != $file && is_file($path . $file)) { + unlink($path . $file); + } + } + } + } +} diff --git a/core/library/think/console/command/Help.php b/core/library/think/console/command/Help.php new file mode 100644 index 00000000..bae2c653 --- /dev/null +++ b/core/library/think/console/command/Help.php @@ -0,0 +1,69 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Option as InputOption; +use think\console\Output; + +class Help extends Command +{ + + private $command; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->ignoreValidationErrors(); + + $this->setName('help')->setDefinition([ + new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), + ])->setDescription('Displays help for a command')->setHelp(<<%command.name% command displays help for a given command: + + php %command.full_name% list + +To display the list of available commands, please use the list command. +EOF + ); + } + + /** + * Sets the command. + * @param Command $command The command to set + */ + public function setCommand(Command $command) + { + $this->command = $command; + } + + /** + * {@inheritdoc} + */ + protected function execute(Input $input, Output $output) + { + if (null === $this->command) { + $this->command = $this->getConsole()->find($input->getArgument('command_name')); + } + + $output->describe($this->command, [ + 'raw_text' => $input->getOption('raw'), + ]); + + $this->command = null; + } +} diff --git a/core/library/think/console/command/Lists.php b/core/library/think/console/command/Lists.php new file mode 100644 index 00000000..084ddaa2 --- /dev/null +++ b/core/library/think/console/command/Lists.php @@ -0,0 +1,74 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\Output; +use think\console\input\Argument as InputArgument; +use think\console\input\Option as InputOption; +use think\console\input\Definition as InputDefinition; + +class Lists extends Command +{ + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName('list')->setDefinition($this->createDefinition())->setDescription('Lists commands')->setHelp(<<%command.name% command lists all commands: + + php %command.full_name% + +You can also display the commands for a specific namespace: + + php %command.full_name% test + +It's also possible to get raw list of commands (useful for embedding command runner): + + php %command.full_name% --raw +EOF + ); + } + + /** + * {@inheritdoc} + */ + public function getNativeDefinition() + { + return $this->createDefinition(); + } + + /** + * {@inheritdoc} + */ + protected function execute(Input $input, Output $output) + { + $output->describe($this->getConsole(), [ + 'raw_text' => $input->getOption('raw'), + 'namespace' => $input->getArgument('namespace'), + ]); + } + + /** + * {@inheritdoc} + */ + private function createDefinition() + { + return new InputDefinition([ + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list') + ]); + } +} diff --git a/core/library/think/console/command/Make.php b/core/library/think/console/command/Make.php new file mode 100644 index 00000000..d1daf34f --- /dev/null +++ b/core/library/think/console/command/Make.php @@ -0,0 +1,110 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\App; +use think\Config; +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\Output; + +abstract class Make extends Command +{ + + protected $type; + + abstract protected function getStub(); + + protected function configure() + { + $this->addArgument('name', Argument::REQUIRED, "The name of the class"); + } + + protected function execute(Input $input, Output $output) + { + + $name = trim($input->getArgument('name')); + + $classname = $this->getClassName($name); + + $pathname = $this->getPathName($classname); + + if (is_file($pathname)) { + $output->writeln('' . $this->type . ' already exists!'); + return false; + } + + if (!is_dir(dirname($pathname))) { + mkdir(strtolower(dirname($pathname)), 0755, true); + } + + file_put_contents($pathname, $this->buildClass($classname)); + + $output->writeln('' . $this->type . ' created successfully.'); + + } + + protected function buildClass($name) + { + $stub = file_get_contents($this->getStub()); + + $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\'); + + $class = str_replace($namespace . '\\', '', $name); + + return str_replace(['{%className%}', '{%namespace%}', '{%app_namespace%}'], [ + $class, + $namespace, + App::$namespace, + ], $stub); + + } + + protected function getPathName($name) + { + $name = str_replace(App::$namespace . '\\', '', $name); + + return APP_PATH . str_replace('\\', '/', $name) . '.php'; + } + + protected function getClassName($name) + { + $appNamespace = App::$namespace; + + if (strpos($name, $appNamespace . '\\') === 0) { + return $name; + } + + if (Config::get('app_multi_module')) { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name, 2); + } else { + $module = 'common'; + } + } else { + $module = null; + } + + if (strpos($name, '/') !== false) { + $name = str_replace('/', '\\', $name); + } + + return $this->getNamespace($appNamespace, $module) . '\\' . $name; + } + + protected function getNamespace($appNamespace, $module) + { + return $module ? ($appNamespace . '\\' . $module) : $appNamespace; + } + +} diff --git a/core/library/think/console/command/make/Controller.php b/core/library/think/console/command/make/Controller.php new file mode 100644 index 00000000..afa7be90 --- /dev/null +++ b/core/library/think/console/command/make/Controller.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\Config; +use think\console\command\Make; +use think\console\input\Option; + +class Controller extends Make +{ + + protected $type = "Controller"; + + protected function configure() + { + parent::configure(); + $this->setName('make:controller') + ->addOption('plain', null, Option::VALUE_NONE, 'Generate an empty controller class.') + ->setDescription('Create a new resource controller class'); + } + + protected function getStub() + { + if ($this->input->getOption('plain')) { + return __DIR__ . '/stubs/controller.plain.stub'; + } + + return __DIR__ . '/stubs/controller.stub'; + } + + protected function getClassName($name) + { + return parent::getClassName($name) . (Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : ''); + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\controller'; + } + +} diff --git a/core/library/think/console/command/make/Model.php b/core/library/think/console/command/make/Model.php new file mode 100644 index 00000000..d4e9b5dd --- /dev/null +++ b/core/library/think/console/command/make/Model.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; + +class Model extends Make +{ + protected $type = "Model"; + + protected function configure() + { + parent::configure(); + $this->setName('make:model') + ->setDescription('Create a new model class'); + } + + protected function getStub() + { + return __DIR__ . '/stubs/model.stub'; + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\model'; + } +} diff --git a/core/library/think/console/command/make/stubs/controller.plain.stub b/core/library/think/console/command/make/stubs/controller.plain.stub new file mode 100644 index 00000000..b7539dcf --- /dev/null +++ b/core/library/think/console/command/make/stubs/controller.plain.stub @@ -0,0 +1,10 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\App; +use think\Config; +use think\console\Command; +use think\console\Input; +use think\console\Output; + +class Autoload extends Command +{ + + protected function configure() + { + $this->setName('optimize:autoload') + ->setDescription('Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.'); + } + + protected function execute(Input $input, Output $output) + { + + $classmapFile = << realpath(rtrim(APP_PATH)), + 'think\\' => LIB_PATH . 'think', + 'behavior\\' => LIB_PATH . 'behavior', + 'traits\\' => LIB_PATH . 'traits', + '' => realpath(rtrim(EXTEND_PATH)), + ]; + + $root_namespace = Config::get('root_namespace'); + foreach ($root_namespace as $namespace => $dir) { + $namespacesToScan[$namespace . '\\'] = realpath($dir); + } + + krsort($namespacesToScan); + $classMap = []; + foreach ($namespacesToScan as $namespace => $dir) { + + if (!is_dir($dir)) { + continue; + } + + $namespaceFilter = $namespace === '' ? null : $namespace; + $classMap = $this->addClassMapCode($dir, $namespaceFilter, $classMap); + } + + ksort($classMap); + foreach ($classMap as $class => $code) { + $classmapFile .= ' ' . var_export($class, true) . ' => ' . $code; + } + $classmapFile .= "];\n"; + + if (!is_dir(RUNTIME_PATH)) { + @mkdir(RUNTIME_PATH, 0755, true); + } + + file_put_contents(RUNTIME_PATH . 'classmap' . EXT, $classmapFile); + + $output->writeln('Succeed!'); + } + + protected function addClassMapCode($dir, $namespace, $classMap) + { + foreach ($this->createMap($dir, $namespace) as $class => $path) { + + $pathCode = $this->getPathCode($path) . ",\n"; + + if (!isset($classMap[$class])) { + $classMap[$class] = $pathCode; + } elseif ($classMap[$class] !== $pathCode && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($classMap[$class] . ' ' . $path, '\\', '/'))) { + $this->output->writeln( + 'Warning: Ambiguous class resolution, "' . $class . '"' . + ' was found in both "' . str_replace(["',\n"], [ + '', + ], $classMap[$class]) . '" and "' . $path . '", the first will be used.' + ); + } + } + return $classMap; + } + + protected function getPathCode($path) + { + + $baseDir = ''; + $libPath = $this->normalizePath(realpath(LIB_PATH)); + $appPath = $this->normalizePath(realpath(APP_PATH)); + $extendPath = $this->normalizePath(realpath(EXTEND_PATH)); + $rootPath = $this->normalizePath(realpath(ROOT_PATH)); + $path = $this->normalizePath($path); + + if ($libPath !== null && strpos($path, $libPath . '/') === 0) { + $path = substr($path, strlen(LIB_PATH)); + $baseDir = 'LIB_PATH'; + } elseif ($appPath !== null && strpos($path, $appPath . '/') === 0) { + $path = substr($path, strlen($appPath) + 1); + $baseDir = 'APP_PATH'; + } elseif ($extendPath !== null && strpos($path, $extendPath . '/') === 0) { + $path = substr($path, strlen($extendPath) + 1); + $baseDir = 'EXTEND_PATH'; + } elseif ($rootPath !== null && strpos($path, $rootPath . '/') === 0) { + $path = substr($path, strlen($rootPath) + 1); + $baseDir = 'ROOT_PATH'; + } + + if ($path !== false) { + $baseDir .= " . "; + } + + return $baseDir . (($path !== false) ? var_export($path, true) : ""); + } + + protected function normalizePath($path) + { + if ($path === false) { + return; + } + $parts = []; + $path = strtr($path, '\\', '/'); + $prefix = ''; + $absolute = false; + + if (preg_match('{^([0-9a-z]+:(?://(?:[a-z]:)?)?)}i', $path, $match)) { + $prefix = $match[1]; + $path = substr($path, strlen($prefix)); + } + + if (substr($path, 0, 1) === '/') { + $absolute = true; + $path = substr($path, 1); + } + + $up = false; + foreach (explode('/', $path) as $chunk) { + if ('..' === $chunk && ($absolute || $up)) { + array_pop($parts); + $up = !(empty($parts) || '..' === end($parts)); + } elseif ('.' !== $chunk && '' !== $chunk) { + $parts[] = $chunk; + $up = '..' !== $chunk; + } + } + + return $prefix . ($absolute ? '/' : '') . implode('/', $parts); + } + + protected function createMap($path, $namespace = null) + { + if (is_string($path)) { + if (is_file($path)) { + $path = [new \SplFileInfo($path)]; + } elseif (is_dir($path)) { + + $objects = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::SELF_FIRST); + + $path = []; + + /** @var \SplFileInfo $object */ + foreach ($objects as $object) { + if ($object->isFile() && $object->getExtension() == 'php') { + $path[] = $object; + } + } + } else { + throw new \RuntimeException( + 'Could not scan for classes inside "' . $path . + '" which does not appear to be a file nor a folder' + ); + } + } + + $map = []; + + /** @var \SplFileInfo $file */ + foreach ($path as $file) { + $filePath = $file->getRealPath(); + + if (pathinfo($filePath, PATHINFO_EXTENSION) != 'php') { + continue; + } + + $classes = $this->findClasses($filePath); + + foreach ($classes as $class) { + if (null !== $namespace && 0 !== strpos($class, $namespace)) { + continue; + } + + if (!isset($map[$class])) { + $map[$class] = $filePath; + } elseif ($map[$class] !== $filePath && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($map[$class] . ' ' . $filePath, '\\', '/'))) { + $this->output->writeln( + 'Warning: Ambiguous class resolution, "' . $class . '"' . + ' was found in both "' . $map[$class] . '" and "' . $filePath . '", the first will be used.' + ); + } + } + } + + return $map; + } + + protected function findClasses($path) + { + $extraTypes = '|trait'; + + $contents = @php_strip_whitespace($path); + if (!$contents) { + if (!file_exists($path)) { + $message = 'File at "%s" does not exist, check your classmap definitions'; + } elseif (!is_readable($path)) { + $message = 'File at "%s" is not readable, check its permissions'; + } elseif ('' === trim(file_get_contents($path))) { + return []; + } else { + $message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted'; + } + $error = error_get_last(); + if (isset($error['message'])) { + $message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message']; + } + throw new \RuntimeException(sprintf($message, $path)); + } + + if (!preg_match('{\b(?:class|interface' . $extraTypes . ')\s}i', $contents)) { + return []; + } + + // strip heredocs/nowdocs + $contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents); + // strip strings + $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); + // strip leading non-php code if needed + if (substr($contents, 0, 2) !== '.+<\?}s', '?>'); + if (false !== $pos && false === strpos(substr($contents, $pos), '])(?Pclass|interface' . $extraTypes . ') \s++ (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) + | \b(?])(?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] + ) + }ix', $contents, $matches); + + $classes = []; + $namespace = ''; + + for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { + if (!empty($matches['ns'][$i])) { + $namespace = str_replace([' ', "\t", "\r", "\n"], '', $matches['nsname'][$i]) . '\\'; + } else { + $name = $matches['name'][$i]; + if ($name[0] === ':') { + $name = 'xhp' . substr(str_replace(['-', ':'], ['_', '__'], $name), 1); + } elseif ($matches['type'][$i] === 'enum') { + $name = rtrim($name, ':'); + } + $classes[] = ltrim($namespace . $name, '\\'); + } + } + + return $classes; + } + +} diff --git a/core/library/think/console/command/optimize/Config.php b/core/library/think/console/command/optimize/Config.php new file mode 100644 index 00000000..cadfe5ee --- /dev/null +++ b/core/library/think/console/command/optimize/Config.php @@ -0,0 +1,93 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\Config as ThinkConfig; +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\Output; + +class Config extends Command +{ + /** @var Output */ + protected $output; + + protected function configure() + { + $this->setName('optimize:config') + ->addArgument('module', Argument::OPTIONAL, 'Build module config cache .') + ->setDescription('Build config and common file cache.'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->hasArgument('module')) { + $module = $input->getArgument('module') . DS; + } else { + $module = ''; + } + + $content = 'buildCacheContent($module); + + if (!is_dir(RUNTIME_PATH . $module)) { + @mkdir(RUNTIME_PATH . $module, 0755, true); + } + + file_put_contents(RUNTIME_PATH . $module . 'init' . EXT, $content); + + $output->writeln('Succeed!'); + } + + protected function buildCacheContent($module) + { + $content = ''; + $path = realpath(APP_PATH . $module) . DS; + + if ($module) { + // 加载模块配置 + $config = ThinkConfig::load(CONF_PATH . $module . 'config' . CONF_EXT); + + // 读取数据库配置文件 + $filename = CONF_PATH . $module . 'database' . CONF_EXT; + ThinkConfig::load($filename, 'database'); + + // 加载应用状态配置 + if ($config['app_status']) { + $config = ThinkConfig::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT); + } + // 读取扩展配置文件 + if (is_dir(CONF_PATH . $module . 'extra')) { + $dir = CONF_PATH . $module . 'extra'; + $files = scandir($dir); + foreach ($files as $file) { + if (strpos($file, CONF_EXT)) { + $filename = $dir . DS . $file; + ThinkConfig::load($filename, pathinfo($file, PATHINFO_FILENAME)); + } + } + } + } + + // 加载行为扩展文件 + if (is_file(CONF_PATH . $module . 'tags' . EXT)) { + $content .= '\think\Hook::import(' . (var_export(include CONF_PATH . $module . 'tags' . EXT, true)) . ');' . PHP_EOL; + } + + // 加载公共文件 + if (is_file($path . 'common' . EXT)) { + $content .= substr(php_strip_whitespace($path . 'common' . EXT), 5) . PHP_EOL; + } + + $content .= '\think\Config::set(' . var_export(ThinkConfig::get(), true) . ');'; + return $content; + } +} diff --git a/core/library/think/console/command/optimize/Route.php b/core/library/think/console/command/optimize/Route.php new file mode 100644 index 00000000..911e4c14 --- /dev/null +++ b/core/library/think/console/command/optimize/Route.php @@ -0,0 +1,70 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\console\Command; +use think\console\Input; +use think\console\Output; + +class Route extends Command +{ + /** @var Output */ + protected $output; + + protected function configure() + { + $this->setName('optimize:route') + ->setDescription('Build route cache.'); + } + + protected function execute(Input $input, Output $output) + { + file_put_contents(RUNTIME_PATH . 'route.php', $this->buildRouteCache()); + $output->writeln('Succeed!'); + } + + protected function buildRouteCache() + { + $files = \think\Config::get('route_config_file'); + foreach ($files as $file) { + if (is_file(CONF_PATH . $file . CONF_EXT)) { + $config = include CONF_PATH . $file . CONF_EXT; + if (is_array($config)) { + \think\Route::import($config); + } + } + } + $rules = \think\Route::rules(true); + array_walk_recursive($rules, [$this, 'buildClosure']); + $content = 'getStartLine(); + $endLine = $reflection->getEndLine(); + $file = $reflection->getFileName(); + $item = file($file); + $content = ''; + for ($i = $startLine - 1; $i <= $endLine - 1; $i++) { + $content .= $item[$i]; + } + $start = strpos($content, 'function'); + $end = strrpos($content, '}'); + $value = '[__start__' . substr($content, $start, $end - $start + 1) . '__end__]'; + } + } +} diff --git a/core/library/think/console/command/optimize/Schema.php b/core/library/think/console/command/optimize/Schema.php new file mode 100644 index 00000000..27eb9dbb --- /dev/null +++ b/core/library/think/console/command/optimize/Schema.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\App; +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\Db; + +class Schema extends Command +{ + /** @var Output */ + protected $output; + + protected function configure() + { + $this->setName('optimize:schema') + ->addOption('config', null, Option::VALUE_REQUIRED, 'db config .') + ->addOption('db', null, Option::VALUE_REQUIRED, 'db name .') + ->addOption('table', null, Option::VALUE_REQUIRED, 'table name .') + ->addOption('module', null, Option::VALUE_REQUIRED, 'module name .') + ->setDescription('Build database schema cache.'); + } + + protected function execute(Input $input, Output $output) + { + if (!is_dir(RUNTIME_PATH . 'schema')) { + @mkdir(RUNTIME_PATH . 'schema', 0755, true); + } + $config = []; + if ($input->hasOption('config')) { + $config = $input->getOption('config'); + } + if ($input->hasOption('module')) { + $module = $input->getOption('module'); + // 读取模型 + $list = scandir(APP_PATH . $module . DS . 'model'); + $app = App::$namespace; + foreach ($list as $file) { + if (0 === strpos($file, '.')) { + continue; + } + $class = '\\' . $app . '\\' . $module . '\\model\\' . pathinfo($file, PATHINFO_FILENAME); + $this->buildModelSchema($class); + } + $output->writeln('Succeed!'); + return; + } elseif ($input->hasOption('table')) { + $table = $input->getOption('table'); + if (!strpos($table, '.')) { + $dbName = Db::connect($config)->getConfig('database'); + } + $tables[] = $table; + } elseif ($input->hasOption('db')) { + $dbName = $input->getOption('db'); + $tables = Db::connect($config)->getTables($dbName); + } elseif (!\think\Config::get('app_multi_module')) { + $app = App::$namespace; + $list = scandir(APP_PATH . 'model'); + foreach ($list as $file) { + if (0 === strpos($file, '.')) { + continue; + } + $class = '\\' . $app . '\\model\\' . pathinfo($file, PATHINFO_FILENAME); + $this->buildModelSchema($class); + } + $output->writeln('Succeed!'); + return; + } else { + $tables = Db::connect($config)->getTables(); + } + + $db = isset($dbName) ? $dbName . '.' : ''; + $this->buildDataBaseSchema($tables, $db, $config); + + $output->writeln('Succeed!'); + } + + protected function buildModelSchema($class) + { + $reflect = new \ReflectionClass($class); + if (!$reflect->isAbstract() && $reflect->isSubclassOf('\think\Model')) { + $table = $class::getTable(); + $dbName = $class::getConfig('database'); + $content = 'getFields($table); + $content .= var_export($info, true) . ';'; + file_put_contents(RUNTIME_PATH . 'schema' . DS . $dbName . '.' . $table . EXT, $content); + } + } + + protected function buildDataBaseSchema($tables, $db, $config) + { + if ('' == $db) { + $dbName = Db::connect($config)->getConfig('database') . '.'; + } else { + $dbName = $db; + } + foreach ($tables as $table) { + $content = 'getFields($db . $table); + $content .= var_export($info, true) . ';'; + file_put_contents(RUNTIME_PATH . 'schema' . DS . $dbName . $table . EXT, $content); + } + } +} diff --git a/core/library/think/console/input/Argument.php b/core/library/think/console/input/Argument.php new file mode 100644 index 00000000..16223bbe --- /dev/null +++ b/core/library/think/console/input/Argument.php @@ -0,0 +1,115 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Argument +{ + + const REQUIRED = 1; + const OPTIONAL = 2; + const IS_ARRAY = 4; + + private $name; + private $mode; + private $default; + private $description; + + /** + * 构造方法 + * @param string $name 参数名 + * @param int $mode 参数类型: self::REQUIRED 或者 self::OPTIONAL + * @param string $description 描述 + * @param mixed $default 默认值 (仅 self::OPTIONAL 类型有效) + * @throws \InvalidArgumentException + */ + public function __construct($name, $mode = null, $description = '', $default = null) + { + if (null === $mode) { + $mode = self::OPTIONAL; + } elseif (!is_int($mode) || $mode > 7 || $mode < 1) { + throw new \InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->mode = $mode; + $this->description = $description; + + $this->setDefault($default); + } + + /** + * 获取参数名 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 是否必须 + * @return bool + */ + public function isRequired() + { + return self::REQUIRED === (self::REQUIRED & $this->mode); + } + + /** + * 该参数是否接受数组 + * @return bool + */ + public function isArray() + { + return self::IS_ARRAY === (self::IS_ARRAY & $this->mode); + } + + /** + * 设置默认值 + * @param mixed $default 默认值 + * @throws \LogicException + */ + public function setDefault($default = null) + { + if (self::REQUIRED === $this->mode && null !== $default) { + throw new \LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!is_array($default)) { + throw new \LogicException('A default value for an array argument must be an array.'); + } + } + + $this->default = $default; + } + + /** + * 获取默认值 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 获取描述 + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/core/library/think/console/input/Definition.php b/core/library/think/console/input/Definition.php new file mode 100644 index 00000000..c71977ec --- /dev/null +++ b/core/library/think/console/input/Definition.php @@ -0,0 +1,375 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Definition +{ + + /** + * @var Argument[] + */ + private $arguments; + + private $requiredCount; + private $hasAnArrayArgument = false; + private $hasOptional; + + /** + * @var Option[] + */ + private $options; + private $shortcuts; + + /** + * 构造方法 + * @param array $definition + * @api + */ + public function __construct(array $definition = []) + { + $this->setDefinition($definition); + } + + /** + * 设置指令的定义 + * @param array $definition 定义的数组 + */ + public function setDefinition(array $definition) + { + $arguments = []; + $options = []; + foreach ($definition as $item) { + if ($item instanceof Option) { + $options[] = $item; + } else { + $arguments[] = $item; + } + } + + $this->setArguments($arguments); + $this->setOptions($options); + } + + /** + * 设置参数 + * @param Argument[] $arguments 参数数组 + */ + public function setArguments($arguments = []) + { + $this->arguments = []; + $this->requiredCount = 0; + $this->hasOptional = false; + $this->hasAnArrayArgument = false; + $this->addArguments($arguments); + } + + /** + * 添加参数 + * @param Argument[] $arguments 参数数组 + * @api + */ + public function addArguments($arguments = []) + { + if (null !== $arguments) { + foreach ($arguments as $argument) { + $this->addArgument($argument); + } + } + } + + /** + * 添加一个参数 + * @param Argument $argument 参数 + * @throws \LogicException + */ + public function addArgument(Argument $argument) + { + if (isset($this->arguments[$argument->getName()])) { + throw new \LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName())); + } + + if ($this->hasAnArrayArgument) { + throw new \LogicException('Cannot add an argument after an array argument.'); + } + + if ($argument->isRequired() && $this->hasOptional) { + throw new \LogicException('Cannot add a required argument after an optional one.'); + } + + if ($argument->isArray()) { + $this->hasAnArrayArgument = true; + } + + if ($argument->isRequired()) { + ++$this->requiredCount; + } else { + $this->hasOptional = true; + } + + $this->arguments[$argument->getName()] = $argument; + } + + /** + * 根据名称或者位置获取参数 + * @param string|int $name 参数名或者位置 + * @return Argument 参数 + * @throws \InvalidArgumentException + */ + public function getArgument($name) + { + if (!$this->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; + + return $arguments[$name]; + } + + /** + * 根据名称或位置检查是否具有某个参数 + * @param string|int $name 参数名或者位置 + * @return bool + * @api + */ + public function hasArgument($name) + { + $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; + + return isset($arguments[$name]); + } + + /** + * 获取所有的参数 + * @return Argument[] 参数数组 + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * 获取参数数量 + * @return int + */ + public function getArgumentCount() + { + return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments); + } + + /** + * 获取必填的参数的数量 + * @return int + */ + public function getArgumentRequiredCount() + { + return $this->requiredCount; + } + + /** + * 获取参数默认值 + * @return array + */ + public function getArgumentDefaults() + { + $values = []; + foreach ($this->arguments as $argument) { + $values[$argument->getName()] = $argument->getDefault(); + } + + return $values; + } + + /** + * 设置选项 + * @param Option[] $options 选项数组 + */ + public function setOptions($options = []) + { + $this->options = []; + $this->shortcuts = []; + $this->addOptions($options); + } + + /** + * 添加选项 + * @param Option[] $options 选项数组 + * @api + */ + public function addOptions($options = []) + { + foreach ($options as $option) { + $this->addOption($option); + } + } + + /** + * 添加一个选项 + * @param Option $option 选项 + * @throws \LogicException + * @api + */ + public function addOption(Option $option) + { + if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) { + throw new \LogicException(sprintf('An option named "%s" already exists.', $option->getName())); + } + + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + if (isset($this->shortcuts[$shortcut]) + && !$option->equals($this->options[$this->shortcuts[$shortcut]]) + ) { + throw new \LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut)); + } + } + } + + $this->options[$option->getName()] = $option; + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + $this->shortcuts[$shortcut] = $option->getName(); + } + } + } + + /** + * 根据名称获取选项 + * @param string $name 选项名 + * @return Option + * @throws \InvalidArgumentException + * @api + */ + public function getOption($name) + { + if (!$this->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); + } + + return $this->options[$name]; + } + + /** + * 根据名称检查是否有这个选项 + * @param string $name 选项名 + * @return bool + * @api + */ + public function hasOption($name) + { + return isset($this->options[$name]); + } + + /** + * 获取所有选项 + * @return Option[] + * @api + */ + public function getOptions() + { + return $this->options; + } + + /** + * 根据名称检查某个选项是否有短名称 + * @param string $name 短名称 + * @return bool + */ + public function hasShortcut($name) + { + return isset($this->shortcuts[$name]); + } + + /** + * 根据短名称获取选项 + * @param string $shortcut 短名称 + * @return Option + */ + public function getOptionForShortcut($shortcut) + { + return $this->getOption($this->shortcutToName($shortcut)); + } + + /** + * 获取所有选项的默认值 + * @return array + */ + public function getOptionDefaults() + { + $values = []; + foreach ($this->options as $option) { + $values[$option->getName()] = $option->getDefault(); + } + + return $values; + } + + /** + * 根据短名称获取选项名 + * @param string $shortcut 短名称 + * @return string + * @throws \InvalidArgumentException + */ + private function shortcutToName($shortcut) + { + if (!isset($this->shortcuts[$shortcut])) { + throw new \InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + return $this->shortcuts[$shortcut]; + } + + /** + * 获取该指令的介绍 + * @param bool $short 是否简洁介绍 + * @return string + */ + public function getSynopsis($short = false) + { + $elements = []; + + if ($short && $this->getOptions()) { + $elements[] = '[options]'; + } elseif (!$short) { + foreach ($this->getOptions() as $option) { + $value = ''; + if ($option->acceptValue()) { + $value = sprintf(' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : ''); + } + + $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; + $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value); + } + } + + if (count($elements) && $this->getArguments()) { + $elements[] = '[--]'; + } + + foreach ($this->getArguments() as $argument) { + $element = '<' . $argument->getName() . '>'; + if (!$argument->isRequired()) { + $element = '[' . $element . ']'; + } elseif ($argument->isArray()) { + $element .= ' (' . $element . ')'; + } + + if ($argument->isArray()) { + $element .= '...'; + } + + $elements[] = $element; + } + + return implode(' ', $elements); + } +} diff --git a/core/library/think/console/input/Option.php b/core/library/think/console/input/Option.php new file mode 100644 index 00000000..e5707c9a --- /dev/null +++ b/core/library/think/console/input/Option.php @@ -0,0 +1,190 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Option +{ + + const VALUE_NONE = 1; + const VALUE_REQUIRED = 2; + const VALUE_OPTIONAL = 4; + const VALUE_IS_ARRAY = 8; + + private $name; + private $shortcut; + private $mode; + private $default; + private $description; + + /** + * 构造方法 + * @param string $name 选项名 + * @param string|array $shortcut 短名称,多个用|隔开或者使用数组 + * @param int $mode 选项类型(可选类型为 self::VALUE_*) + * @param string $description 描述 + * @param mixed $default 默认值 (类型为 self::VALUE_REQUIRED 或者 self::VALUE_NONE 的时候必须为null) + * @throws \InvalidArgumentException + */ + public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + if (0 === strpos($name, '--')) { + $name = substr($name, 2); + } + + if (empty($name)) { + throw new \InvalidArgumentException('An option name cannot be empty.'); + } + + if (empty($shortcut)) { + $shortcut = null; + } + + if (null !== $shortcut) { + if (is_array($shortcut)) { + $shortcut = implode('|', $shortcut); + } + $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); + $shortcuts = array_filter($shortcuts); + $shortcut = implode('|', $shortcuts); + + if (empty($shortcut)) { + throw new \InvalidArgumentException('An option shortcut cannot be empty.'); + } + } + + if (null === $mode) { + $mode = self::VALUE_NONE; + } elseif (!is_int($mode) || $mode > 15 || $mode < 1) { + throw new \InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->shortcut = $shortcut; + $this->mode = $mode; + $this->description = $description; + + if ($this->isArray() && !$this->acceptValue()) { + throw new \InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); + } + + $this->setDefault($default); + } + + /** + * 获取短名称 + * @return string + */ + public function getShortcut() + { + return $this->shortcut; + } + + /** + * 获取选项名 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 是否可以设置值 + * @return bool 类型不是 self::VALUE_NONE 的时候返回true,其他均返回false + */ + public function acceptValue() + { + return $this->isValueRequired() || $this->isValueOptional(); + } + + /** + * 是否必须 + * @return bool 类型是 self::VALUE_REQUIRED 的时候返回true,其他均返回false + */ + public function isValueRequired() + { + return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); + } + + /** + * 是否可选 + * @return bool 类型是 self::VALUE_OPTIONAL 的时候返回true,其他均返回false + */ + public function isValueOptional() + { + return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); + } + + /** + * 选项值是否接受数组 + * @return bool 类型是 self::VALUE_IS_ARRAY 的时候返回true,其他均返回false + */ + public function isArray() + { + return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); + } + + /** + * 设置默认值 + * @param mixed $default 默认值 + * @throws \LogicException + */ + public function setDefault($default = null) + { + if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { + throw new \LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!is_array($default)) { + throw new \LogicException('A default value for an array option must be an array.'); + } + } + + $this->default = $this->acceptValue() ? $default : false; + } + + /** + * 获取默认值 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 获取描述文字 + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * 检查所给选项是否是当前这个 + * @param Option $option + * @return bool + */ + public function equals(Option $option) + { + return $option->getName() === $this->getName() + && $option->getShortcut() === $this->getShortcut() + && $option->getDefault() === $this->getDefault() + && $option->isArray() === $this->isArray() + && $option->isValueRequired() === $this->isValueRequired() + && $option->isValueOptional() === $this->isValueOptional(); + } +} diff --git a/core/library/think/console/output/Ask.php b/core/library/think/console/output/Ask.php new file mode 100644 index 00000000..3933eb29 --- /dev/null +++ b/core/library/think/console/output/Ask.php @@ -0,0 +1,340 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +use think\console\Input; +use think\console\Output; +use think\console\output\question\Choice; +use think\console\output\question\Confirmation; + +class Ask +{ + private static $stty; + + private static $shell; + + /** @var Input */ + protected $input; + + /** @var Output */ + protected $output; + + /** @var Question */ + protected $question; + + public function __construct(Input $input, Output $output, Question $question) + { + $this->input = $input; + $this->output = $output; + $this->question = $question; + } + + public function run() + { + if (!$this->input->isInteractive()) { + return $this->question->getDefault(); + } + + if (!$this->question->getValidator()) { + return $this->doAsk(); + } + + $that = $this; + + $interviewer = function () use ($that) { + return $that->doAsk(); + }; + + return $this->validateAttempts($interviewer); + } + + protected function doAsk() + { + $this->writePrompt(); + + $inputStream = STDIN; + $autocomplete = $this->question->getAutocompleterValues(); + + if (null === $autocomplete || !$this->hasSttyAvailable()) { + $ret = false; + if ($this->question->isHidden()) { + try { + $ret = trim($this->getHiddenResponse($inputStream)); + } catch (\RuntimeException $e) { + if (!$this->question->isHiddenFallback()) { + throw $e; + } + } + } + + if (false === $ret) { + $ret = fgets($inputStream, 4096); + if (false === $ret) { + throw new \RuntimeException('Aborted'); + } + $ret = trim($ret); + } + } else { + $ret = trim($this->autocomplete($inputStream)); + } + + $ret = strlen($ret) > 0 ? $ret : $this->question->getDefault(); + + if ($normalizer = $this->question->getNormalizer()) { + return $normalizer($ret); + } + + return $ret; + } + + private function autocomplete($inputStream) + { + $autocomplete = $this->question->getAutocompleterValues(); + $ret = ''; + + $i = 0; + $ofs = -1; + $matches = $autocomplete; + $numMatches = count($matches); + + $sttyMode = shell_exec('stty -g'); + + shell_exec('stty -icanon -echo'); + + while (!feof($inputStream)) { + $c = fread($inputStream, 1); + + if ("\177" === $c) { + if (0 === $numMatches && 0 !== $i) { + --$i; + $this->output->write("\033[1D"); + } + + if ($i === 0) { + $ofs = -1; + $matches = $autocomplete; + $numMatches = count($matches); + } else { + $numMatches = 0; + } + + $ret = substr($ret, 0, $i); + } elseif ("\033" === $c) { + $c .= fread($inputStream, 2); + + if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { + if ('A' === $c[2] && -1 === $ofs) { + $ofs = 0; + } + + if (0 === $numMatches) { + continue; + } + + $ofs += ('A' === $c[2]) ? -1 : 1; + $ofs = ($numMatches + $ofs) % $numMatches; + } + } elseif (ord($c) < 32) { + if ("\t" === $c || "\n" === $c) { + if ($numMatches > 0 && -1 !== $ofs) { + $ret = $matches[$ofs]; + $this->output->write(substr($ret, $i)); + $i = strlen($ret); + } + + if ("\n" === $c) { + $this->output->write($c); + break; + } + + $numMatches = 0; + } + + continue; + } else { + $this->output->write($c); + $ret .= $c; + ++$i; + + $numMatches = 0; + $ofs = 0; + + foreach ($autocomplete as $value) { + if (0 === strpos($value, $ret) && $i !== strlen($value)) { + $matches[$numMatches++] = $value; + } + } + } + + $this->output->write("\033[K"); + + if ($numMatches > 0 && -1 !== $ofs) { + $this->output->write("\0337"); + $this->output->highlight(substr($matches[$ofs], $i)); + $this->output->write("\0338"); + } + } + + shell_exec(sprintf('stty %s', $sttyMode)); + + return $ret; + } + + protected function getHiddenResponse($inputStream) + { + if ('\\' === DIRECTORY_SEPARATOR) { + $exe = __DIR__ . '/../bin/hiddeninput.exe'; + + $value = rtrim(shell_exec($exe)); + $this->output->writeln(''); + + if (isset($tmpExe)) { + unlink($tmpExe); + } + + return $value; + } + + if ($this->hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + + shell_exec('stty -echo'); + $value = fgets($inputStream, 4096); + shell_exec(sprintf('stty %s', $sttyMode)); + + if (false === $value) { + throw new \RuntimeException('Aborted'); + } + + $value = trim($value); + $this->output->writeln(''); + + return $value; + } + + if (false !== $shell = $this->getShell()) { + $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword'; + $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); + $value = rtrim(shell_exec($command)); + $this->output->writeln(''); + + return $value; + } + + throw new \RuntimeException('Unable to hide the response.'); + } + + protected function validateAttempts($interviewer) + { + /** @var \Exception $error */ + $error = null; + $attempts = $this->question->getMaxAttempts(); + while (null === $attempts || $attempts--) { + if (null !== $error) { + $this->output->error($error->getMessage()); + } + + try { + return call_user_func($this->question->getValidator(), $interviewer()); + } catch (\Exception $error) { + } + } + + throw $error; + } + + /** + * 显示问题的提示信息 + */ + protected function writePrompt() + { + $text = $this->question->getQuestion(); + $default = $this->question->getDefault(); + + switch (true) { + case null === $default: + $text = sprintf(' %s:', $text); + + break; + + case $this->question instanceof Confirmation: + $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no'); + + break; + + case $this->question instanceof Choice && $this->question->isMultiselect(): + $choices = $this->question->getChoices(); + $default = explode(',', $default); + + foreach ($default as $key => $value) { + $default[$key] = $choices[trim($value)]; + } + + $text = sprintf(' %s [%s]:', $text, implode(', ', $default)); + + break; + + case $this->question instanceof Choice: + $choices = $this->question->getChoices(); + $text = sprintf(' %s [%s]:', $text, $choices[$default]); + + break; + + default: + $text = sprintf(' %s [%s]:', $text, $default); + } + + $this->output->writeln($text); + + if ($this->question instanceof Choice) { + $width = max(array_map('strlen', array_keys($this->question->getChoices()))); + + foreach ($this->question->getChoices() as $key => $value) { + $this->output->writeln(sprintf(" [%-${width}s] %s", $key, $value)); + } + } + + $this->output->write(' > '); + } + + private function getShell() + { + if (null !== self::$shell) { + return self::$shell; + } + + self::$shell = false; + + if (file_exists('/usr/bin/env')) { + $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; + foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) { + if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { + self::$shell = $sh; + break; + } + } + } + + return self::$shell; + } + + private function hasSttyAvailable() + { + if (null !== self::$stty) { + return self::$stty; + } + + exec('stty 2>&1', $output, $exitcode); + + return self::$stty = $exitcode === 0; + } +} diff --git a/core/library/think/console/output/Descriptor.php b/core/library/think/console/output/Descriptor.php new file mode 100644 index 00000000..6d98d53c --- /dev/null +++ b/core/library/think/console/output/Descriptor.php @@ -0,0 +1,319 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +use think\Console; +use think\console\Command; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; +use think\console\output\descriptor\Console as ConsoleDescription; + +class Descriptor +{ + + /** + * @var Output + */ + protected $output; + + /** + * {@inheritdoc} + */ + public function describe(Output $output, $object, array $options = []) + { + $this->output = $output; + + switch (true) { + case $object instanceof InputArgument: + $this->describeInputArgument($object, $options); + break; + case $object instanceof InputOption: + $this->describeInputOption($object, $options); + break; + case $object instanceof InputDefinition: + $this->describeInputDefinition($object, $options); + break; + case $object instanceof Command: + $this->describeCommand($object, $options); + break; + case $object instanceof Console: + $this->describeConsole($object, $options); + break; + default: + throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object))); + } + } + + /** + * 输出内容 + * @param string $content + * @param bool $decorated + */ + protected function write($content, $decorated = false) + { + $this->output->write($content, false, $decorated ? Output::OUTPUT_NORMAL : Output::OUTPUT_RAW); + } + + /** + * 描述参数 + * @param InputArgument $argument + * @param array $options + * @return string|mixed + */ + protected function describeInputArgument(InputArgument $argument, array $options = []) + { + if (null !== $argument->getDefault() + && (!is_array($argument->getDefault()) + || count($argument->getDefault())) + ) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault())); + } else { + $default = ''; + } + + $totalWidth = isset($options['total_width']) ? $options['total_width'] : strlen($argument->getName()); + $spacingWidth = $totalWidth - strlen($argument->getName()) + 2; + + $this->writeText(sprintf(" %s%s%s%s", $argument->getName(), str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces + preg_replace('/\s*\R\s*/', PHP_EOL . str_repeat(' ', $totalWidth + 17), $argument->getDescription()), $default), $options); + } + + /** + * 描述选项 + * @param InputOption $option + * @param array $options + * @return string|mixed + */ + protected function describeInputOption(InputOption $option, array $options = []) + { + if ($option->acceptValue() && null !== $option->getDefault() + && (!is_array($option->getDefault()) + || count($option->getDefault())) + ) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault())); + } else { + $default = ''; + } + + $value = ''; + if ($option->acceptValue()) { + $value = '=' . strtoupper($option->getName()); + + if ($option->isValueOptional()) { + $value = '[' . $value . ']'; + } + } + + $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]); + $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', sprintf('--%s%s', $option->getName(), $value)); + + $spacingWidth = $totalWidth - strlen($synopsis) + 2; + + $this->writeText(sprintf(" %s%s%s%s%s", $synopsis, str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces + preg_replace('/\s*\R\s*/', "\n" . str_repeat(' ', $totalWidth + 17), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed)' : ''), $options); + } + + /** + * 描述输入 + * @param InputDefinition $definition + * @param array $options + * @return string|mixed + */ + protected function describeInputDefinition(InputDefinition $definition, array $options = []) + { + $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); + foreach ($definition->getArguments() as $argument) { + $totalWidth = max($totalWidth, strlen($argument->getName())); + } + + if ($definition->getArguments()) { + $this->writeText('Arguments:', $options); + $this->writeText("\n"); + foreach ($definition->getArguments() as $argument) { + $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth])); + $this->writeText("\n"); + } + } + + if ($definition->getArguments() && $definition->getOptions()) { + $this->writeText("\n"); + } + + if ($definition->getOptions()) { + $laterOptions = []; + + $this->writeText('Options:', $options); + foreach ($definition->getOptions() as $option) { + if (strlen($option->getShortcut()) > 1) { + $laterOptions[] = $option; + continue; + } + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + foreach ($laterOptions as $option) { + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + } + } + + /** + * 描述指令 + * @param Command $command + * @param array $options + * @return string|mixed + */ + protected function describeCommand(Command $command, array $options = []) + { + $command->getSynopsis(true); + $command->getSynopsis(false); + $command->mergeConsoleDefinition(false); + + $this->writeText('Usage:', $options); + foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) { + $this->writeText("\n"); + $this->writeText(' ' . $usage, $options); + } + $this->writeText("\n"); + + $definition = $command->getNativeDefinition(); + if ($definition->getOptions() || $definition->getArguments()) { + $this->writeText("\n"); + $this->describeInputDefinition($definition, $options); + $this->writeText("\n"); + } + + if ($help = $command->getProcessedHelp()) { + $this->writeText("\n"); + $this->writeText('Help:', $options); + $this->writeText("\n"); + $this->writeText(' ' . str_replace("\n", "\n ", $help), $options); + $this->writeText("\n"); + } + } + + /** + * 描述控制台 + * @param Console $console + * @param array $options + * @return string|mixed + */ + protected function describeConsole(Console $console, array $options = []) + { + $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; + $description = new ConsoleDescription($console, $describedNamespace); + + if (isset($options['raw_text']) && $options['raw_text']) { + $width = $this->getColumnWidth($description->getCommands()); + + foreach ($description->getCommands() as $command) { + $this->writeText(sprintf("%-${width}s %s", $command->getName(), $command->getDescription()), $options); + $this->writeText("\n"); + } + } else { + if ('' != $help = $console->getHelp()) { + $this->writeText("$help\n\n", $options); + } + + $this->writeText("Usage:\n", $options); + $this->writeText(" command [options] [arguments]\n\n", $options); + + $this->describeInputDefinition(new InputDefinition($console->getDefinition()->getOptions()), $options); + + $this->writeText("\n"); + $this->writeText("\n"); + + $width = $this->getColumnWidth($description->getCommands()); + + if ($describedNamespace) { + $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options); + } else { + $this->writeText('Available commands:', $options); + } + + // add commands by namespace + foreach ($description->getNamespaces() as $namespace) { + if (!$describedNamespace && ConsoleDescription::GLOBAL_NAMESPACE !== $namespace['id']) { + $this->writeText("\n"); + $this->writeText(' ' . $namespace['id'] . '', $options); + } + + foreach ($namespace['commands'] as $name) { + $this->writeText("\n"); + $spacingWidth = $width - strlen($name); + $this->writeText(sprintf(" %s%s%s", $name, str_repeat(' ', $spacingWidth), $description->getCommand($name) + ->getDescription()), $options); + } + } + + $this->writeText("\n"); + } + } + + /** + * {@inheritdoc} + */ + private function writeText($content, array $options = []) + { + $this->write(isset($options['raw_text']) + && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true); + } + + /** + * 格式化 + * @param mixed $default + * @return string + */ + private function formatDefaultValue($default) + { + return json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + /** + * @param Command[] $commands + * @return int + */ + private function getColumnWidth(array $commands) + { + $width = 0; + foreach ($commands as $command) { + $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width; + } + + return $width + 2; + } + + /** + * @param InputOption[] $options + * @return int + */ + private function calculateTotalWidthForOptions($options) + { + $totalWidth = 0; + foreach ($options as $option) { + $nameLength = 4 + strlen($option->getName()) + 2; // - + shortcut + , + whitespace + name + -- + + if ($option->acceptValue()) { + $valueLength = 1 + strlen($option->getName()); // = + value + $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ] + + $nameLength += $valueLength; + } + $totalWidth = max($totalWidth, $nameLength); + } + + return $totalWidth; + } +} diff --git a/core/library/think/console/output/Formatter.php b/core/library/think/console/output/Formatter.php new file mode 100644 index 00000000..f8bee552 --- /dev/null +++ b/core/library/think/console/output/Formatter.php @@ -0,0 +1,198 @@ + +// +---------------------------------------------------------------------- +namespace think\console\output; + +use think\console\output\formatter\Stack as StyleStack; +use think\console\output\formatter\Style; + +class Formatter +{ + + private $decorated = false; + private $styles = []; + private $styleStack; + + /** + * 转义 + * @param string $text + * @return string + */ + public static function escape($text) + { + return preg_replace('/([^\\\\]?)setStyle('error', new Style('white', 'red')); + $this->setStyle('info', new Style('green')); + $this->setStyle('comment', new Style('yellow')); + $this->setStyle('question', new Style('black', 'cyan')); + $this->setStyle('highlight', new Style('red')); + $this->setStyle('warning', new Style('black', 'yellow')); + + $this->styleStack = new StyleStack(); + } + + /** + * 设置外观标识 + * @param bool $decorated 是否美化文字 + */ + public function setDecorated($decorated) + { + $this->decorated = (bool) $decorated; + } + + /** + * 获取外观标识 + * @return bool + */ + public function isDecorated() + { + return $this->decorated; + } + + /** + * 添加一个新样式 + * @param string $name 样式名 + * @param Style $style 样式实例 + */ + public function setStyle($name, Style $style) + { + $this->styles[strtolower($name)] = $style; + } + + /** + * 是否有这个样式 + * @param string $name + * @return bool + */ + public function hasStyle($name) + { + return isset($this->styles[strtolower($name)]); + } + + /** + * 获取样式 + * @param string $name + * @return Style + * @throws \InvalidArgumentException + */ + public function getStyle($name) + { + if (!$this->hasStyle($name)) { + throw new \InvalidArgumentException(sprintf('Undefined style: %s', $name)); + } + + return $this->styles[strtolower($name)]; + } + + /** + * 使用所给的样式格式化文字 + * @param string $message 文字 + * @return string + */ + public function format($message) + { + $offset = 0; + $output = ''; + $tagRegex = '[a-z][a-z0-9_=;-]*'; + preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#isx", $message, $matches, PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $i => $match) { + $pos = $match[1]; + $text = $match[0]; + + if (0 != $pos && '\\' == $message[$pos - 1]) { + continue; + } + + $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset)); + $offset = $pos + strlen($text); + + if ($open = '/' != $text[1]) { + $tag = $matches[1][$i][0]; + } else { + $tag = isset($matches[3][$i][0]) ? $matches[3][$i][0] : ''; + } + + if (!$open && !$tag) { + // + $this->styleStack->pop(); + } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) { + $output .= $this->applyCurrentStyle($text); + } elseif ($open) { + $this->styleStack->push($style); + } else { + $this->styleStack->pop($style); + } + } + + $output .= $this->applyCurrentStyle(substr($message, $offset)); + + return str_replace('\\<', '<', $output); + } + + /** + * @return StyleStack + */ + public function getStyleStack() + { + return $this->styleStack; + } + + /** + * 根据字符串创建新的样式实例 + * @param string $string + * @return Style|bool + */ + private function createStyleFromString($string) + { + if (isset($this->styles[$string])) { + return $this->styles[$string]; + } + + if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($string), $matches, PREG_SET_ORDER)) { + return false; + } + + $style = new Style(); + foreach ($matches as $match) { + array_shift($match); + + if ('fg' == $match[0]) { + $style->setForeground($match[1]); + } elseif ('bg' == $match[0]) { + $style->setBackground($match[1]); + } else { + try { + $style->setOption($match[1]); + } catch (\InvalidArgumentException $e) { + return false; + } + } + } + + return $style; + } + + /** + * 从堆栈应用样式到文字 + * @param string $text 文字 + * @return string + */ + private function applyCurrentStyle($text) + { + return $this->isDecorated() && strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text; + } +} diff --git a/core/library/think/console/output/Question.php b/core/library/think/console/output/Question.php new file mode 100644 index 00000000..03975f27 --- /dev/null +++ b/core/library/think/console/output/Question.php @@ -0,0 +1,211 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +class Question +{ + + private $question; + private $attempts; + private $hidden = false; + private $hiddenFallback = true; + private $autocompleterValues; + private $validator; + private $default; + private $normalizer; + + /** + * 构造方法 + * @param string $question 问题 + * @param mixed $default 默认答案 + */ + public function __construct($question, $default = null) + { + $this->question = $question; + $this->default = $default; + } + + /** + * 获取问题 + * @return string + */ + public function getQuestion() + { + return $this->question; + } + + /** + * 获取默认答案 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 是否隐藏答案 + * @return bool + */ + public function isHidden() + { + return $this->hidden; + } + + /** + * 隐藏答案 + * @param bool $hidden + * @return Question + */ + public function setHidden($hidden) + { + if ($this->autocompleterValues) { + throw new \LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->hidden = (bool) $hidden; + + return $this; + } + + /** + * 不能被隐藏是否撤销 + * @return bool + */ + public function isHiddenFallback() + { + return $this->hiddenFallback; + } + + /** + * 设置不能被隐藏的时候的操作 + * @param bool $fallback + * @return Question + */ + public function setHiddenFallback($fallback) + { + $this->hiddenFallback = (bool) $fallback; + + return $this; + } + + /** + * 获取自动完成 + * @return null|array|\Traversable + */ + public function getAutocompleterValues() + { + return $this->autocompleterValues; + } + + /** + * 设置自动完成的值 + * @param null|array|\Traversable $values + * @return Question + * @throws \InvalidArgumentException + * @throws \LogicException + */ + public function setAutocompleterValues($values) + { + if (is_array($values) && $this->isAssoc($values)) { + $values = array_merge(array_keys($values), array_values($values)); + } + + if (null !== $values && !is_array($values)) { + if (!$values instanceof \Traversable || $values instanceof \Countable) { + throw new \InvalidArgumentException('Autocompleter values can be either an array, `null` or an object implementing both `Countable` and `Traversable` interfaces.'); + } + } + + if ($this->hidden) { + throw new \LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->autocompleterValues = $values; + + return $this; + } + + /** + * 设置答案的验证器 + * @param null|callable $validator + * @return Question The current instance + */ + public function setValidator($validator) + { + $this->validator = $validator; + + return $this; + } + + /** + * 获取验证器 + * @return null|callable + */ + public function getValidator() + { + return $this->validator; + } + + /** + * 设置最大重试次数 + * @param null|int $attempts + * @return Question + * @throws \InvalidArgumentException + */ + public function setMaxAttempts($attempts) + { + if (null !== $attempts && $attempts < 1) { + throw new \InvalidArgumentException('Maximum number of attempts must be a positive value.'); + } + + $this->attempts = $attempts; + + return $this; + } + + /** + * 获取最大重试次数 + * @return null|int + */ + public function getMaxAttempts() + { + return $this->attempts; + } + + /** + * 设置响应的回调 + * @param string|\Closure $normalizer + * @return Question + */ + public function setNormalizer($normalizer) + { + $this->normalizer = $normalizer; + + return $this; + } + + /** + * 获取响应回调 + * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. + * @return string|\Closure + */ + public function getNormalizer() + { + return $this->normalizer; + } + + protected function isAssoc($array) + { + return (bool) count(array_filter(array_keys($array), 'is_string')); + } +} diff --git a/core/library/think/console/output/descriptor/Console.php b/core/library/think/console/output/descriptor/Console.php new file mode 100644 index 00000000..4648b68e --- /dev/null +++ b/core/library/think/console/output/descriptor/Console.php @@ -0,0 +1,149 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\descriptor; + +use think\Console as ThinkConsole; +use think\console\Command; + +class Console +{ + + const GLOBAL_NAMESPACE = '_global'; + + /** + * @var ThinkConsole + */ + private $console; + + /** + * @var null|string + */ + private $namespace; + + /** + * @var array + */ + private $namespaces; + + /** + * @var Command[] + */ + private $commands; + + /** + * @var Command[] + */ + private $aliases; + + /** + * 构造方法 + * @param ThinkConsole $console + * @param string|null $namespace + */ + public function __construct(ThinkConsole $console, $namespace = null) + { + $this->console = $console; + $this->namespace = $namespace; + } + + /** + * @return array + */ + public function getNamespaces() + { + if (null === $this->namespaces) { + $this->inspectConsole(); + } + + return $this->namespaces; + } + + /** + * @return Command[] + */ + public function getCommands() + { + if (null === $this->commands) { + $this->inspectConsole(); + } + + return $this->commands; + } + + /** + * @param string $name + * @return Command + * @throws \InvalidArgumentException + */ + public function getCommand($name) + { + if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { + throw new \InvalidArgumentException(sprintf('Command %s does not exist.', $name)); + } + + return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name]; + } + + private function inspectConsole() + { + $this->commands = []; + $this->namespaces = []; + + $all = $this->console->all($this->namespace ? $this->console->findNamespace($this->namespace) : null); + foreach ($this->sortCommands($all) as $namespace => $commands) { + $names = []; + + /** @var Command $command */ + foreach ($commands as $name => $command) { + if (!$command->getName()) { + continue; + } + + if ($command->getName() === $name) { + $this->commands[$name] = $command; + } else { + $this->aliases[$name] = $command; + } + + $names[] = $name; + } + + $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names]; + } + } + + /** + * @param array $commands + * @return array + */ + private function sortCommands(array $commands) + { + $namespacedCommands = []; + foreach ($commands as $name => $command) { + $key = $this->console->extractNamespace($name, 1); + if (!$key) { + $key = self::GLOBAL_NAMESPACE; + } + + $namespacedCommands[$key][$name] = $command; + } + ksort($namespacedCommands); + + foreach ($namespacedCommands as &$commandsSet) { + ksort($commandsSet); + } + // unset reference to keep scope clear + unset($commandsSet); + + return $namespacedCommands; + } +} diff --git a/core/library/think/console/output/driver/Buffer.php b/core/library/think/console/output/driver/Buffer.php new file mode 100644 index 00000000..c77a2ec4 --- /dev/null +++ b/core/library/think/console/output/driver/Buffer.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; + +class Buffer +{ + /** + * @var string + */ + private $buffer = ''; + + public function __construct(Output $output) + { + // do nothing + } + + public function fetch() + { + $content = $this->buffer; + $this->buffer = ''; + return $content; + } + + public function write($messages, $newline = false, $options = Output::OUTPUT_NORMAL) + { + $messages = (array) $messages; + + foreach ($messages as $message) { + $this->buffer .= $message; + } + if ($newline) { + $this->buffer .= "\n"; + } + } + + public function renderException(\Exception $e) + { + // do nothing + } + +} diff --git a/core/library/think/console/output/driver/Console.php b/core/library/think/console/output/driver/Console.php new file mode 100644 index 00000000..8f29fd02 --- /dev/null +++ b/core/library/think/console/output/driver/Console.php @@ -0,0 +1,373 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; +use think\console\output\Formatter; + +class Console +{ + + /** @var Resource */ + private $stdout; + + /** @var Formatter */ + private $formatter; + + private $terminalDimensions; + + /** @var Output */ + private $output; + + public function __construct(Output $output) + { + $this->output = $output; + $this->formatter = new Formatter(); + $this->stdout = $this->openOutputStream(); + $decorated = $this->hasColorSupport($this->stdout); + $this->formatter->setDecorated($decorated); + } + + public function getFormatter() + { + return $this->formatter; + } + + public function setDecorated($decorated) + { + $this->formatter->setDecorated($decorated); + } + + public function write($messages, $newline = false, $type = Output::OUTPUT_NORMAL, $stream = null) + { + if (Output::VERBOSITY_QUIET === $this->output->getVerbosity()) { + return; + } + + $messages = (array) $messages; + + foreach ($messages as $message) { + switch ($type) { + case Output::OUTPUT_NORMAL: + $message = $this->formatter->format($message); + break; + case Output::OUTPUT_RAW: + break; + case Output::OUTPUT_PLAIN: + $message = strip_tags($this->formatter->format($message)); + break; + default: + throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type)); + } + + $this->doWrite($message, $newline, $stream); + } + } + + public function renderException(\Exception $e) + { + $stderr = $this->openErrorStream(); + $decorated = $this->hasColorSupport($stderr); + $this->formatter->setDecorated($decorated); + + do { + $title = sprintf(' [%s] ', get_class($e)); + + $len = $this->stringWidth($title); + + $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX; + + if (defined('HHVM_VERSION') && $width > 1 << 31) { + $width = 1 << 31; + } + $lines = []; + foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) { + foreach ($this->splitStringByWidth($line, $width - 4) as $line) { + + $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $line)) + 4; + $lines[] = [$line, $lineLength]; + + $len = max($lineLength, $len); + } + } + + $messages = ['', '']; + $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len)); + $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title)))); + foreach ($lines as $line) { + $messages[] = sprintf(' %s %s', $line[0], str_repeat(' ', $len - $line[1])); + } + $messages[] = $emptyLine; + $messages[] = ''; + $messages[] = ''; + + $this->write($messages, true, Output::OUTPUT_NORMAL, $stderr); + + if (Output::VERBOSITY_VERBOSE <= $this->output->getVerbosity()) { + $this->write('Exception trace:', true, Output::OUTPUT_NORMAL, $stderr); + + // exception related properties + $trace = $e->getTrace(); + array_unshift($trace, [ + 'function' => '', + 'file' => $e->getFile() !== null ? $e->getFile() : 'n/a', + 'line' => $e->getLine() !== null ? $e->getLine() : 'n/a', + 'args' => [], + ]); + + for ($i = 0, $count = count($trace); $i < $count; ++$i) { + $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; + $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; + $function = $trace[$i]['function']; + $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a'; + $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; + + $this->write(sprintf(' %s%s%s() at %s:%s', $class, $type, $function, $file, $line), true, Output::OUTPUT_NORMAL, $stderr); + } + + $this->write('', true, Output::OUTPUT_NORMAL, $stderr); + $this->write('', true, Output::OUTPUT_NORMAL, $stderr); + } + } while ($e = $e->getPrevious()); + + } + + /** + * 获取终端宽度 + * @return int|null + */ + protected function getTerminalWidth() + { + $dimensions = $this->getTerminalDimensions(); + + return $dimensions[0]; + } + + /** + * 获取终端高度 + * @return int|null + */ + protected function getTerminalHeight() + { + $dimensions = $this->getTerminalDimensions(); + + return $dimensions[1]; + } + + /** + * 获取当前终端的尺寸 + * @return array + */ + public function getTerminalDimensions() + { + if ($this->terminalDimensions) { + return $this->terminalDimensions; + } + + if ('\\' === DS) { + if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) { + return [(int) $matches[1], (int) $matches[2]]; + } + if (preg_match('/^(\d+)x(\d+)$/', $this->getMode(), $matches)) { + return [(int) $matches[1], (int) $matches[2]]; + } + } + + if ($sttyString = $this->getSttyColumns()) { + if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { + return [(int) $matches[2], (int) $matches[1]]; + } + if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { + return [(int) $matches[2], (int) $matches[1]]; + } + } + + return [null, null]; + } + + /** + * 获取stty列数 + * @return string + */ + private function getSttyColumns() + { + if (!function_exists('proc_open')) { + return; + } + + $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + return $info; + } + return; + } + + /** + * 获取终端模式 + * @return string x 或 null + */ + private function getMode() + { + if (!function_exists('proc_open')) { + return; + } + + $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open('mode CON', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { + return $matches[2] . 'x' . $matches[1]; + } + } + return; + } + + private function stringWidth($string) + { + if (!function_exists('mb_strwidth')) { + return strlen($string); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return strlen($string); + } + + return mb_strwidth($string, $encoding); + } + + private function splitStringByWidth($string, $width) + { + if (!function_exists('mb_strwidth')) { + return str_split($string, $width); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return str_split($string, $width); + } + + $utf8String = mb_convert_encoding($string, 'utf8', $encoding); + $lines = []; + $line = ''; + foreach (preg_split('//u', $utf8String) as $char) { + if (mb_strwidth($line . $char, 'utf8') <= $width) { + $line .= $char; + continue; + } + $lines[] = str_pad($line, $width); + $line = $char; + } + if (strlen($line)) { + $lines[] = count($lines) ? str_pad($line, $width) : $line; + } + + mb_convert_variables($encoding, 'utf8', $lines); + + return $lines; + } + + private function isRunningOS400() + { + $checks = [ + function_exists('php_uname') ? php_uname('s') : '', + getenv('OSTYPE'), + PHP_OS, + ]; + return false !== stripos(implode(';', $checks), 'OS400'); + } + + /** + * 当前环境是否支持写入控制台输出到stdout. + * + * @return bool + */ + protected function hasStdoutSupport() + { + return false === $this->isRunningOS400(); + } + + /** + * 当前环境是否支持写入控制台输出到stderr. + * + * @return bool + */ + protected function hasStderrSupport() + { + return false === $this->isRunningOS400(); + } + + /** + * @return resource + */ + private function openOutputStream() + { + if (!$this->hasStdoutSupport()) { + return fopen('php://output', 'w'); + } + return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w'); + } + + /** + * @return resource + */ + private function openErrorStream() + { + return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w'); + } + + /** + * 将消息写入到输出。 + * @param string $message 消息 + * @param bool $newline 是否另起一行 + * @param null $stream + */ + protected function doWrite($message, $newline, $stream = null) + { + if (null === $stream) { + $stream = $this->stdout; + } + if (false === @fwrite($stream, $message . ($newline ? PHP_EOL : ''))) { + throw new \RuntimeException('Unable to write output.'); + } + + fflush($stream); + } + + /** + * 是否支持着色 + * @param $stream + * @return bool + */ + protected function hasColorSupport($stream) + { + if (DIRECTORY_SEPARATOR === '\\') { + return + '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD + || false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + || 'xterm' === getenv('TERM'); + } + + return function_exists('posix_isatty') && @posix_isatty($stream); + } + +} diff --git a/core/library/think/console/output/driver/Nothing.php b/core/library/think/console/output/driver/Nothing.php new file mode 100644 index 00000000..9a55f777 --- /dev/null +++ b/core/library/think/console/output/driver/Nothing.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; + +class Nothing +{ + + public function __construct(Output $output) + { + // do nothing + } + + public function write($messages, $newline = false, $options = Output::OUTPUT_NORMAL) + { + // do nothing + } + + public function renderException(\Exception $e) + { + // do nothing + } +} diff --git a/core/library/think/console/output/formatter/Stack.php b/core/library/think/console/output/formatter/Stack.php new file mode 100644 index 00000000..4864a3f2 --- /dev/null +++ b/core/library/think/console/output/formatter/Stack.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\formatter; + +class Stack +{ + + /** + * @var Style[] + */ + private $styles; + + /** + * @var Style + */ + private $emptyStyle; + + /** + * 构造方法 + * @param Style|null $emptyStyle + */ + public function __construct(Style $emptyStyle = null) + { + $this->emptyStyle = $emptyStyle ?: new Style(); + $this->reset(); + } + + /** + * 重置堆栈 + */ + public function reset() + { + $this->styles = []; + } + + /** + * 推一个样式进入堆栈 + * @param Style $style + */ + public function push(Style $style) + { + $this->styles[] = $style; + } + + /** + * 从堆栈中弹出一个样式 + * @param Style|null $style + * @return Style + * @throws \InvalidArgumentException + */ + public function pop(Style $style = null) + { + if (empty($this->styles)) { + return $this->emptyStyle; + } + + if (null === $style) { + return array_pop($this->styles); + } + + /** + * @var int $index + * @var Style $stackedStyle + */ + foreach (array_reverse($this->styles, true) as $index => $stackedStyle) { + if ($style->apply('') === $stackedStyle->apply('')) { + $this->styles = array_slice($this->styles, 0, $index); + + return $stackedStyle; + } + } + + throw new \InvalidArgumentException('Incorrectly nested style tag found.'); + } + + /** + * 计算堆栈的当前样式。 + * @return Style + */ + public function getCurrent() + { + if (empty($this->styles)) { + return $this->emptyStyle; + } + + return $this->styles[count($this->styles) - 1]; + } + + /** + * @param Style $emptyStyle + * @return Stack + */ + public function setEmptyStyle(Style $emptyStyle) + { + $this->emptyStyle = $emptyStyle; + + return $this; + } + + /** + * @return Style + */ + public function getEmptyStyle() + { + return $this->emptyStyle; + } +} diff --git a/core/library/think/console/output/formatter/Style.php b/core/library/think/console/output/formatter/Style.php new file mode 100644 index 00000000..d9b09998 --- /dev/null +++ b/core/library/think/console/output/formatter/Style.php @@ -0,0 +1,189 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\formatter; + +class Style +{ + + private static $availableForegroundColors = [ + 'black' => ['set' => 30, 'unset' => 39], + 'red' => ['set' => 31, 'unset' => 39], + 'green' => ['set' => 32, 'unset' => 39], + 'yellow' => ['set' => 33, 'unset' => 39], + 'blue' => ['set' => 34, 'unset' => 39], + 'magenta' => ['set' => 35, 'unset' => 39], + 'cyan' => ['set' => 36, 'unset' => 39], + 'white' => ['set' => 37, 'unset' => 39], + ]; + private static $availableBackgroundColors = [ + 'black' => ['set' => 40, 'unset' => 49], + 'red' => ['set' => 41, 'unset' => 49], + 'green' => ['set' => 42, 'unset' => 49], + 'yellow' => ['set' => 43, 'unset' => 49], + 'blue' => ['set' => 44, 'unset' => 49], + 'magenta' => ['set' => 45, 'unset' => 49], + 'cyan' => ['set' => 46, 'unset' => 49], + 'white' => ['set' => 47, 'unset' => 49], + ]; + private static $availableOptions = [ + 'bold' => ['set' => 1, 'unset' => 22], + 'underscore' => ['set' => 4, 'unset' => 24], + 'blink' => ['set' => 5, 'unset' => 25], + 'reverse' => ['set' => 7, 'unset' => 27], + 'conceal' => ['set' => 8, 'unset' => 28], + ]; + + private $foreground; + private $background; + private $options = []; + + /** + * 初始化输出的样式 + * @param string|null $foreground 字体颜色 + * @param string|null $background 背景色 + * @param array $options 格式 + * @api + */ + public function __construct($foreground = null, $background = null, array $options = []) + { + if (null !== $foreground) { + $this->setForeground($foreground); + } + if (null !== $background) { + $this->setBackground($background); + } + if (count($options)) { + $this->setOptions($options); + } + } + + /** + * 设置字体颜色 + * @param string|null $color 颜色名 + * @throws \InvalidArgumentException + * @api + */ + public function setForeground($color = null) + { + if (null === $color) { + $this->foreground = null; + + return; + } + + if (!isset(static::$availableForegroundColors[$color])) { + throw new \InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableForegroundColors)))); + } + + $this->foreground = static::$availableForegroundColors[$color]; + } + + /** + * 设置背景色 + * @param string|null $color 颜色名 + * @throws \InvalidArgumentException + * @api + */ + public function setBackground($color = null) + { + if (null === $color) { + $this->background = null; + + return; + } + + if (!isset(static::$availableBackgroundColors[$color])) { + throw new \InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableBackgroundColors)))); + } + + $this->background = static::$availableBackgroundColors[$color]; + } + + /** + * 设置字体格式 + * @param string $option 格式名 + * @throws \InvalidArgumentException When the option name isn't defined + * @api + */ + public function setOption($option) + { + if (!isset(static::$availableOptions[$option])) { + throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)))); + } + + if (!in_array(static::$availableOptions[$option], $this->options)) { + $this->options[] = static::$availableOptions[$option]; + } + } + + /** + * 重置字体格式 + * @param string $option 格式名 + * @throws \InvalidArgumentException + */ + public function unsetOption($option) + { + if (!isset(static::$availableOptions[$option])) { + throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)))); + } + + $pos = array_search(static::$availableOptions[$option], $this->options); + if (false !== $pos) { + unset($this->options[$pos]); + } + } + + /** + * 批量设置字体格式 + * @param array $options + */ + public function setOptions(array $options) + { + $this->options = []; + + foreach ($options as $option) { + $this->setOption($option); + } + } + + /** + * 应用样式到文字 + * @param string $text 文字 + * @return string + */ + public function apply($text) + { + $setCodes = []; + $unsetCodes = []; + + if (null !== $this->foreground) { + $setCodes[] = $this->foreground['set']; + $unsetCodes[] = $this->foreground['unset']; + } + if (null !== $this->background) { + $setCodes[] = $this->background['set']; + $unsetCodes[] = $this->background['unset']; + } + if (count($this->options)) { + foreach ($this->options as $option) { + $setCodes[] = $option['set']; + $unsetCodes[] = $option['unset']; + } + } + + if (0 === count($setCodes)) { + return $text; + } + + return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes)); + } +} diff --git a/core/library/think/console/output/question/Choice.php b/core/library/think/console/output/question/Choice.php new file mode 100644 index 00000000..f6760e5e --- /dev/null +++ b/core/library/think/console/output/question/Choice.php @@ -0,0 +1,163 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\question; + +use think\console\output\Question; + +class Choice extends Question +{ + + private $choices; + private $multiselect = false; + private $prompt = ' > '; + private $errorMessage = 'Value "%s" is invalid'; + + /** + * 构造方法 + * @param string $question 问题 + * @param array $choices 选项 + * @param mixed $default 默认答案 + */ + public function __construct($question, array $choices, $default = null) + { + parent::__construct($question, $default); + + $this->choices = $choices; + $this->setValidator($this->getDefaultValidator()); + $this->setAutocompleterValues($choices); + } + + /** + * 可选项 + * @return array + */ + public function getChoices() + { + return $this->choices; + } + + /** + * 设置可否多选 + * @param bool $multiselect + * @return self + */ + public function setMultiselect($multiselect) + { + $this->multiselect = $multiselect; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + public function isMultiselect() + { + return $this->multiselect; + } + + /** + * 获取提示 + * @return string + */ + public function getPrompt() + { + return $this->prompt; + } + + /** + * 设置提示 + * @param string $prompt + * @return self + */ + public function setPrompt($prompt) + { + $this->prompt = $prompt; + + return $this; + } + + /** + * 设置错误提示信息 + * @param string $errorMessage + * @return self + */ + public function setErrorMessage($errorMessage) + { + $this->errorMessage = $errorMessage; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + /** + * 获取默认的验证方法 + * @return callable + */ + private function getDefaultValidator() + { + $choices = $this->choices; + $errorMessage = $this->errorMessage; + $multiselect = $this->multiselect; + $isAssoc = $this->isAssoc($choices); + + return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) { + // Collapse all spaces. + $selectedChoices = str_replace(' ', '', $selected); + + if ($multiselect) { + // Check for a separated comma values + if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) { + throw new \InvalidArgumentException(sprintf($errorMessage, $selected)); + } + $selectedChoices = explode(',', $selectedChoices); + } else { + $selectedChoices = [$selected]; + } + + $multiselectChoices = []; + foreach ($selectedChoices as $value) { + $results = []; + foreach ($choices as $key => $choice) { + if ($choice === $value) { + $results[] = $key; + } + } + + if (count($results) > 1) { + throw new \InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of %s.', implode(' or ', $results))); + } + + $result = array_search($value, $choices); + + if (!$isAssoc) { + if (!empty($result)) { + $result = $choices[$result]; + } elseif (isset($choices[$value])) { + $result = $choices[$value]; + } + } elseif (empty($result) && array_key_exists($value, $choices)) { + $result = $value; + } + + if (empty($result)) { + throw new \InvalidArgumentException(sprintf($errorMessage, $value)); + } + array_push($multiselectChoices, $result); + } + + if ($multiselect) { + return $multiselectChoices; + } + + return current($multiselectChoices); + }; + } +} diff --git a/core/library/think/console/output/question/Confirmation.php b/core/library/think/console/output/question/Confirmation.php new file mode 100644 index 00000000..6598f9b3 --- /dev/null +++ b/core/library/think/console/output/question/Confirmation.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\question; + +use think\console\output\Question; + +class Confirmation extends Question +{ + + private $trueAnswerRegex; + + /** + * 构造方法 + * @param string $question 问题 + * @param bool $default 默认答案 + * @param string $trueAnswerRegex 验证正则 + */ + public function __construct($question, $default = true, $trueAnswerRegex = '/^y/i') + { + parent::__construct($question, (bool) $default); + + $this->trueAnswerRegex = $trueAnswerRegex; + $this->setNormalizer($this->getDefaultNormalizer()); + } + + /** + * 获取默认的答案回调 + * @return callable + */ + private function getDefaultNormalizer() + { + $default = $this->getDefault(); + $regex = $this->trueAnswerRegex; + + return function ($answer) use ($default, $regex) { + if (is_bool($answer)) { + return $answer; + } + + $answerIsTrue = (bool) preg_match($regex, $answer); + if (false === $default) { + return $answer && $answerIsTrue; + } + + return !$answer || $answerIsTrue; + }; + } +} diff --git a/core/library/think/db/Builder.php b/core/library/think/db/Builder.php index 23f88836..5d57cebb 100644 --- a/core/library/think/db/Builder.php +++ b/core/library/think/db/Builder.php @@ -22,7 +22,7 @@ abstract class Builder protected $query; // 数据库表达式 - protected $exp = ['eq' => '=', 'neq' => '<>', 'gt' => '>', 'egt' => '>=', 'lt' => '<', 'elt' => '<=', 'notlike' => 'NOT LIKE', 'like' => 'LIKE', 'in' => 'IN', 'exp' => 'EXP', 'notin' => 'NOT IN', 'not in' => 'NOT IN', 'between' => 'BETWEEN', 'not between' => 'NOT BETWEEN', 'notbetween' => 'NOT BETWEEN', 'exists' => 'EXISTS', 'notexists' => 'NOT EXISTS', 'not exists' => 'NOT EXISTS', 'null' => 'NULL', 'notnull' => 'NOT NULL', 'not null' => 'NOT NULL', '> time' => '> TIME', '< time' => '< TIME', '>= time' => '>= TIME', '<= time' => '<= TIME', 'between time' => 'BETWEEN TIME', 'not between time' => 'NOT BETWEEN TIME', 'notbetween time' => 'NOT BETWEEN TIME']; + protected $exp = ['eq' => '=', 'neq' => '<>', 'gt' => '>', 'egt' => '>=', 'lt' => '<', 'elt' => '<=', 'notlike' => 'NOT LIKE', 'not like' => 'NOT LIKE', 'like' => 'LIKE', 'in' => 'IN', 'exp' => 'EXP', 'notin' => 'NOT IN', 'not in' => 'NOT IN', 'between' => 'BETWEEN', 'not between' => 'NOT BETWEEN', 'notbetween' => 'NOT BETWEEN', 'exists' => 'EXISTS', 'notexists' => 'NOT EXISTS', 'not exists' => 'NOT EXISTS', 'null' => 'NULL', 'notnull' => 'NOT NULL', 'not null' => 'NOT NULL', '> time' => '> TIME', '< time' => '< TIME', '>= time' => '>= TIME', '<= time' => '<= TIME', 'between time' => 'BETWEEN TIME', 'not between time' => 'NOT BETWEEN TIME', 'notbetween time' => 'NOT BETWEEN TIME']; // SQL表达式 protected $selectSql = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%'; @@ -199,7 +199,7 @@ abstract class Builder $key = strstr($key, '@think', true); } $key = $this->parseSqlTable($key); - $item[] = $this->parseKey($key) . ' ' . $this->parseKey($table); + $item[] = $this->parseKey($key) . ' ' . (isset($options['alias'][$table]) ? $this->parseKey($options['alias'][$table]) : $this->parseKey($table)); } else { $table = $this->parseSqlTable($table); if (isset($options['alias'][$table])) { @@ -380,11 +380,13 @@ abstract class Builder if (array_key_exists($field, $binds)) { $bind = []; $array = []; - foreach ($value as $k => $v) { - if ($this->query->isBind($bindName . '_in_' . $k)) { - $bindKey = $bindName . '_in_' . uniqid() . '_' . $k; + $i = 0; + foreach ($value as $v) { + $i++; + if ($this->query->isBind($bindName . '_in_' . $i)) { + $bindKey = $bindName . '_in_' . uniqid() . '_' . $i; } else { - $bindKey = $bindName . '_in_' . $k; + $bindKey = $bindName . '_in_' . $i; } $bind[$bindKey] = [$v, $bindType]; $array[] = ':' . $bindKey; diff --git a/core/library/think/db/Connection.php b/core/library/think/db/Connection.php index fa773e51..2c2b9a8c 100644 --- a/core/library/think/db/Connection.php +++ b/core/library/think/db/Connection.php @@ -386,7 +386,7 @@ abstract class Connection return $this->close()->query($sql, $bind, $master, $pdo); } throw new PDOException($e, $this->config, $this->getLastsql()); - } catch (\ErrorException $e) { + } catch (\Exception $e) { if ($this->isBreak($e)) { return $this->close()->query($sql, $bind, $master, $pdo); } @@ -449,7 +449,7 @@ abstract class Connection return $this->close()->execute($sql, $bind); } throw new PDOException($e, $this->config, $this->getLastsql()); - } catch (\ErrorException $e) { + } catch (\Exception $e) { if ($this->isBreak($e)) { return $this->close()->execute($sql, $bind); } diff --git a/core/library/think/db/Query.php b/core/library/think/db/Query.php index a8722a1a..1ab48f46 100644 --- a/core/library/think/db/Query.php +++ b/core/library/think/db/Query.php @@ -2301,7 +2301,7 @@ class Query $key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options) . serialize($this->bind)); $resultSet = Cache::get($key); } - if (!$resultSet) { + if (false === $resultSet) { // 生成查询SQL $sql = $this->builder->select($options); // 获取参数绑定 @@ -2323,7 +2323,7 @@ class Query } } - if (isset($cache) && $resultSet) { + if (isset($cache) && false !== $resultSet) { // 缓存数据集 $this->cacheData($key, $resultSet, $cache); } @@ -2481,7 +2481,7 @@ class Query $result = isset($resultSet[0]) ? $resultSet[0] : null; } - if (isset($cache) && $result) { + if (isset($cache) && false !== $result) { // 缓存数据 $this->cacheData($key, $result, $cache); } diff --git a/core/library/think/exception/DbException.php b/core/library/think/exception/DbException.php index 656d6913..532af5ef 100644 --- a/core/library/think/exception/DbException.php +++ b/core/library/think/exception/DbException.php @@ -36,6 +36,7 @@ class DbException extends Exception 'Error SQL' => $sql, ]); + unset($config['username'], $config['password']); $this->setData('Database Config', $config); } diff --git a/core/library/think/model/relation/BelongsToMany.php b/core/library/think/model/relation/BelongsToMany.php index 71ab4266..ef08abb4 100644 --- a/core/library/think/model/relation/BelongsToMany.php +++ b/core/library/think/model/relation/BelongsToMany.php @@ -105,9 +105,8 @@ class BelongsToMany extends Relation { $foreignKey = $this->foreignKey; $localKey = $this->localKey; - $middle = $this->middle; + $pk = $this->parent->getPk(); // 关联查询 - $pk = $this->parent->getPk(); $condition['pivot.' . $localKey] = $this->parent->$pk; return $this->belongsToManyQuery($foreignKey, $localKey, $condition); } diff --git a/core/library/think/response/Redirect.php b/core/library/think/response/Redirect.php index f2002779..f45b0e1c 100644 --- a/core/library/think/response/Redirect.php +++ b/core/library/think/response/Redirect.php @@ -67,7 +67,7 @@ class Redirect extends Response */ public function getTargetUrl() { - return (strpos($this->data, '://') || 0 === strpos($this->data, '/')) ? $this->data : Url::build($this->data, $this->params); + return strpos($this->data, '://') ? $this->data : Url::build($this->data, $this->params); } public function params($params = [])