diff --git a/app/common.php b/app/common.php index 55d22f26..708c5dc8 100644 --- a/app/common.php +++ b/app/common.php @@ -1,12 +1,1177 @@ +// | Author: molong // +---------------------------------------------------------------------- -// 应用公共文件 +// SentCMS常量定义 +define('SENTCMS_VERSION', '3.6.201803'); +define('SENT_ADDON_PATH', __DIR__ . '/../addons' . DS); + +//字符串解密加密 +function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) { + $ckey_length = 4; // 随机密钥长度 取值 0-32; + // 加入随机密钥,可以令密文无任何规律,即便是原文和密钥完全相同,加密结果也会每次不同,增大破解难度。 + // 取值越大,密文变动规律越大,密文变化 = 16 的 $ckey_length 次方 + // 当此值为 0 时,则不产生随机密钥 + $uc_key = config('data_auth_key') ? config('data_auth_key') : 'sentcms'; + $key = md5($key ? $key : $uc_key); + $keya = md5(substr($key, 0, 16)); + $keyb = md5(substr($key, 16, 16)); + $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : ''; + + $cryptkey = $keya . md5($keya . $keyc); + $key_length = strlen($cryptkey); + + $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string; + + $string_length = strlen($string); + $result = ''; + $box = range(0, 255); + $rndkey = array(); + for ($i = 0; $i <= 255; $i++) { + $rndkey[$i] = ord($cryptkey[$i % $key_length]); + } + + for ($j = $i = 0; $i < 256; $i++) { + $j = ($j + $box[$i] + $rndkey[$i]) % 256; + $tmp = $box[$i]; + $box[$i] = $box[$j]; + $box[$j] = $tmp; + } + + for ($a = $j = $i = 0; $i < $string_length; $i++) { + $a = ($a + 1) % 256; + $j = ($j + $box[$a]) % 256; + $tmp = $box[$a]; + $box[$a] = $box[$j]; + $box[$j] = $tmp; + $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); + } + + if ($operation == 'DECODE') { + if ((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) { + return substr($result, 26); + } else { + return ''; + } + } else { + return $keyc . str_replace('=', '', base64_encode($result)); + } +} + +/** ++---------------------------------------------------------- + * 产生随机字串,可用来自动生成密码 默认长度6位 字母和数字混合 ++---------------------------------------------------------- + * @param string $len 长度 + * @param string $type 字串类型 + * 0 字母 1 数字 其它 混合 + * @param string $addChars 额外字符 ++---------------------------------------------------------- + * @return string ++---------------------------------------------------------- + */ +function rand_string($len = 6, $type = '', $addChars = '') { + $str = ''; + switch ($type) { + case 0: + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' . $addChars; + break; + case 1: + $chars = str_repeat('0123456789', 3); + break; + case 2: + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' . $addChars; + break; + case 3: + $chars = 'abcdefghijklmnopqrstuvwxyz' . $addChars; + break; + case 4: + $chars = "们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书术状厂须离再目海交权且儿青才证低越际八试规斯近注办布门铁需走议县兵固除般引齿千胜细影济白格效置推空配刀叶率述今选养德话查差半敌始片施响收华觉备名红续均药标记难存测士身紧液派准斤角降维板许破述技消底床田势端感往神便贺村构照容非搞亚磨族火段算适讲按值美态黄易彪服早班麦削信排台声该击素张密害侯草何树肥继右属市严径螺检左页抗苏显苦英快称坏移约巴材省黑武培著河帝仅针怎植京助升王眼她抓含苗副杂普谈围食射源例致酸旧却充足短划剂宣环落首尺波承粉践府鱼随考刻靠够满夫失包住促枝局菌杆周护岩师举曲春元超负砂封换太模贫减阳扬江析亩木言球朝医校古呢稻宋听唯输滑站另卫字鼓刚写刘微略范供阿块某功套友限项余倒卷创律雨让骨远帮初皮播优占死毒圈伟季训控激找叫云互跟裂粮粒母练塞钢顶策双留误础吸阻故寸盾晚丝女散焊功株亲院冷彻弹错散商视艺灭版烈零室轻血倍缺厘泵察绝富城冲喷壤简否柱李望盘磁雄似困巩益洲脱投送奴侧润盖挥距触星松送获兴独官混纪依未突架宽冬章湿偏纹吃执阀矿寨责熟稳夺硬价努翻奇甲预职评读背协损棉侵灰虽矛厚罗泥辟告卵箱掌氧恩爱停曾溶营终纲孟钱待尽俄缩沙退陈讨奋械载胞幼哪剥迫旋征槽倒握担仍呀鲜吧卡粗介钻逐弱脚怕盐末阴丰雾冠丙街莱贝辐肠付吉渗瑞惊顿挤秒悬姆烂森糖圣凹陶词迟蚕亿矩康遵牧遭幅园腔订香肉弟屋敏恢忘编印蜂急拿扩伤飞露核缘游振操央伍域甚迅辉异序免纸夜乡久隶缸夹念兰映沟乙吗儒杀汽磷艰晶插埃燃欢铁补咱芽永瓦倾阵碳演威附牙芽永瓦斜灌欧献顺猪洋腐请透司危括脉宜笑若尾束壮暴企菜穗楚汉愈绿拖牛份染既秋遍锻玉夏疗尖殖井费州访吹荣铜沿替滚客召旱悟刺脑措贯藏敢令隙炉壳硫煤迎铸粘探临薄旬善福纵择礼愿伏残雷延烟句纯渐耕跑泽慢栽鲁赤繁境潮横掉锥希池败船假亮谓托伙哲怀割摆贡呈劲财仪沉炼麻罪祖息车穿货销齐鼠抽画饲龙库守筑房歌寒喜哥洗蚀废纳腹乎录镜妇恶脂庄擦险赞钟摇典柄辩竹谷卖乱虚桥奥伯赶垂途额壁网截野遗静谋弄挂课镇妄盛耐援扎虑键归符庆聚绕摩忙舞遇索顾胶羊湖钉仁音迹碎伸灯避泛亡答勇频皇柳哈揭甘诺概宪浓岛袭谁洪谢炮浇斑讯懂灵蛋闭孩释乳巨徒私银伊景坦累匀霉杜乐勒隔弯绩招绍胡呼痛峰零柴簧午跳居尚丁秦稍追梁折耗碱殊岗挖氏刃剧堆赫荷胸衡勤膜篇登驻案刊秧缓凸役剪川雪链渔啦脸户洛孢勃盟买杨宗焦赛旗滤硅炭股坐蒸凝竟陷枪黎救冒暗洞犯筒您宋弧爆谬涂味津臂障褐陆啊健尊豆拔莫抵桑坡缝警挑污冰柬嘴啥饭塑寄赵喊垫丹渡耳刨虎笔稀昆浪萨茶滴浅拥穴覆伦娘吨浸袖珠雌妈紫戏塔锤震岁貌洁剖牢锋疑霸闪埔猛诉刷狠忽灾闹乔唐漏闻沈熔氯荒茎男凡抢像浆旁玻亦忠唱蒙予纷捕锁尤乘乌智淡允叛畜俘摸锈扫毕璃宝芯爷鉴秘净蒋钙肩腾枯抛轨堂拌爸循诱祝励肯酒绳穷塘燥泡袋朗喂铝软渠颗惯贸粪综墙趋彼届墨碍启逆卸航衣孙龄岭骗休借" . $addChars; + break; + default: + // 默认去掉了容易混淆的字符oOLl和数字01,要添加请使用addChars参数 + $chars = 'ABCDEFGHIJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789' . $addChars; + break; + } + if ($len > 10) { + //位数过长重复字符串一定次数 + $chars = $type == 1 ? str_repeat($chars, $len) : str_repeat($chars, 5); + } + if ($type != 4) { + $chars = str_shuffle($chars); + $str = substr($chars, 0, $len); + } else { + // 中文随机字 + for ($i = 0; $i < $len; $i++) { + $str .= msubstr($chars, floor(mt_rand(0, mb_strlen($chars, 'utf-8') - 1)), 1); + } + } + return $str; +} + +/** + * 字符串截取,支持中文和其他编码 + * @static + * @access public + * @param string $str 需要转换的字符串 + * @param string $start 开始位置 + * @param string $length 截取长度 + * @param string $charset 编码格式 + * @param string $suffix 截断显示字符 + * @return string + */ +function msubstr($str, $start = 0, $length, $charset = "utf-8", $suffix = true) { + if (function_exists("mb_substr")) { + $slice = mb_substr($str, $start, $length, $charset); + } elseif (function_exists('iconv_substr')) { + $slice = iconv_substr($str, $start, $length, $charset); + if (false === $slice) { + $slice = ''; + } + } else { + $re['utf-8'] = "/[\x01-\x7f]|[\xc2-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xff][\x80-\xbf]{3}/"; + $re['gb2312'] = "/[\x01-\x7f]|[\xb0-\xf7][\xa0-\xfe]/"; + $re['gbk'] = "/[\x01-\x7f]|[\x81-\xfe][\x40-\xfe]/"; + $re['big5'] = "/[\x01-\x7f]|[\x81-\xfe]([\x40-\x7e]|\xa1-\xfe])/"; + preg_match_all($re[$charset], $str, $match); + $slice = join("", array_slice($match[0], $start, $length)); + } + if (strlen($slice) == strlen($str)) { + return $slice; + } else { + return $suffix ? $slice . '...' : $slice; + } +} + +/** + * 处理插件钩子 + * @param string $hook 钩子名称 + * @param mixed $params 传入参数 + * @return void + */ +function hook($hook, $params = array()) { + \think\Hook::listen($hook, $params); +} + +/** + * 获取广告位广告 + * @param string $name 广告位名称 + * @param mixed $params 传入参数 + * @return html + */ +function ad($name, $param = array()) { + return widget('common/Ad/run', array('name' => $name)); +} + +/** + * 获取插件类的类名 + * @param strng $name 插件名 + */ +function get_addon_class($name) { + $class = "\\addons\\" . strtolower($name) . "\\{$name}"; + return $class; +} + +/** + * 获取插件类的配置文件数组 + * @param string $name 插件名 + */ +function get_addon_config($name) { + $class = get_addon_class($name); + if (class_exists($class)) { + $addon = new $class(); + return $addon->getConfig(); + } else { + return array(); + } +} + +/** + * 插件显示内容里生成访问插件的url + * @param string $url url + * @param array $param 参数 + * @author 麦当苗儿 + */ +function addons_url($url, $param = array()) { + $url = parse_url($url); + $case = config('URL_CASE_INSENSITIVE'); + $addons = $case ? parse_name($url['scheme']) : $url['scheme']; + $controller = $case ? parse_name($url['host']) : $url['host']; + $action = trim($case ? strtolower($url['path']) : $url['path'], '/'); + + /* 解析URL带的参数 */ + if (isset($url['query'])) { + parse_str($url['query'], $query); + $param = array_merge($query, $param); + } + + /* 基础参数 */ + $params = array( + 'mc' => $addons, + 'op' => $controller, + 'ac' => $action, + ); + $params = array_merge($params, $param); //添加额外参数 + + return \think\Url::build('index/addons/execute', $params); +} + +/** + * 获取导航URL + * @param string $url 导航URL + * @return string 解析或的url + * @author 麦当苗儿 + */ +function get_nav_url($url) { + switch ($url) { + case 'http://' === substr($url, 0, 7): + case 'https://' === substr($url, 0, 8): + case '#' === substr($url, 0, 1): + break; + default: + $url = \think\Url::build($url); + break; + } + return $url; +} + +/** + * 获取文档封面图片 + * @param int $cover_id + * @param string $field + * @return 完整的数据 或者 指定的$field字段值 + * @author huajie + */ +function get_cover($cover_id, $field = null) { + if (empty($cover_id)) { + return BASE_PATH . '/static/images/default.png'; + } + $picture = db('Picture')->where(array('status' => 1, 'id' => $cover_id))->find(); + if ($field == 'path') { + if (!empty($picture['url'])) { + $picture['path'] = $picture['url'] ? BASE_PATH . $picture['url'] : BASE_PATH . '/static/images/default.png'; + } else { + $picture['path'] = $picture['path'] ? BASE_PATH . $picture['path'] : BASE_PATH . '/static/images/default.png'; + } + } + return empty($field) ? $picture : $picture[$field]; +} + +/** + * 获取文件 + * @param int $file_id + * @param string $field + * @return 完整的数据 或者 指定的$field字段值 + * @author huajie + */ +function get_file($file_id, $field = null) { + if (empty($file_id)) { + return ''; + } + $file = db('File')->where(array('id' => $file_id))->find(); + if ($field == 'path') { + return $file['savepath']; + } elseif ($field == 'time') { + return date('Y-m-d H:i:s', $file['create_time']); + } + return empty($field) ? $file : $file[$field]; +} + +/** + * 获取多图地址 + * @param array $covers + * @return 返回图片列表 + * @author molong + */ +function get_cover_list($covers) { + if ($covers == '') { + return false; + } + $cover_list = explode(',', $covers); + foreach ($cover_list as $item) { + $list[] = get_cover($item, 'path'); + } + return $list; +} + +/** + * 字符串命名风格转换 + * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格 + * @param string $name 字符串 + * @param integer $type 转换类型 + * @return string + */ +function parse_name($name, $type = 0) { + if ($type) { + return ucfirst(preg_replace_callback('/_([a-zA-Z])/', function ($match) {return strtoupper($match[1]);}, $name)); + } else { + return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_")); + } +} + +// 不区分大小写的in_array实现 +function in_array_case($value, $array) { + return in_array(strtolower($value), array_map('strtolower', $array)); +} + +/** + * 数据签名认证 + * @param array $data 被认证的数据 + * @return string 签名 + * @author 麦当苗儿 + */ +function data_auth_sign($data) { + //数据类型检测 + if (!is_array($data)) { + $data = (array) $data; + } + ksort($data); //排序 + $code = http_build_query($data); //url编码并生成query字符串 + $sign = sha1($code); //生成签名 + return $sign; +} + +/** + * 检测用户是否登录 + * @return integer 0-未登录,大于0-当前登录用户ID + * @author 麦当苗儿 + */ +function is_login() { + $user = session('user_auth'); + if (empty($user)) { + return 0; + } else { + return session('user_auth_sign') == data_auth_sign($user) ? $user['uid'] : 0; + } +} + +/** + * 检测当前用户是否为管理员 + * @return boolean true-管理员,false-非管理员 + * @author 麦当苗儿 + */ +function is_administrator($uid = null) { + $uid = is_null($uid) ? is_login() : $uid; + return $uid && (intval($uid) === config('user_administrator')); +} + +/** + * 获取客户端IP地址 + * @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 + * @param boolean $adv 是否进行高级模式获取(有可能被伪装) + * @return mixed + */ +function get_client_ip($type = 0, $adv = false) { + $type = $type ? 1 : 0; + static $ip = NULL; + if ($ip !== NULL) { + return $ip[$type]; + } + + if ($adv) { + if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + $pos = array_search('unknown', $arr); + if (false !== $pos) { + unset($arr[$pos]); + } + + $ip = trim($arr[0]); + } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + } elseif (isset($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + } elseif (isset($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + // IP地址合法验证 + $long = sprintf("%u", ip2long($ip)); + $ip = $long ? array($ip, $long) : array('0.0.0.0', 0); + return $ip[$type]; +} + +/** + * 时间戳格式化 + * @param int $time + * @return string 完整的时间显示 + * @author huajie + */ +function time_format($time = NULL, $format = 'Y-m-d H:i') { + $time = $time === NULL ? time() : intval($time); + return date($format, $time); +} + +/** + * 根据用户ID获取用户名 + * @param integer $uid 用户ID + * @return string 用户名 + */ +function get_username($uid = 0) { + static $list; + if (!($uid && is_numeric($uid))) { + //获取当前登录用户名 + return session('user_auth.username'); + } + $name = db('member')->where(array('uid' => $uid))->value('username'); + return $name; +} + +/** + * 根据用户ID获取用户昵称 + * @param integer $uid 用户ID + * @return string 用户昵称 + */ +function get_nickname($uid = 0) { + static $list; + if (!($uid && is_numeric($uid))) { + //获取当前登录用户名 + return session('user_auth.username'); + } + + /* 获取缓存数据 */ + if (empty($list)) { + $list = cache('sys_user_nickname_list'); + } + + /* 查找用户信息 */ + $key = "u{$uid}"; + if (isset($list[$key])) { + //已缓存,直接使用 + $name = $list[$key]; + } else { + //调用接口获取用户信息 + $info = db('Member')->field('nickname')->find($uid); + if ($info !== false && $info['nickname']) { + $nickname = $info['nickname']; + $name = $list[$key] = $nickname; + /* 缓存用户 */ + $count = count($list); + $max = config('USER_MAX_CACHE'); + while ($count-- > $max) { + array_shift($list); + } + cache('sys_user_nickname_list', $list); + } else { + $name = ''; + } + } + return $name; +} + +/** + * 对查询结果集进行排序 + * @access public + * @param array $list 查询结果 + * @param string $field 排序的字段名 + * @param array $sortby 排序类型 + * asc正向排序 desc逆向排序 nat自然排序 + * @return array + */ +function list_sort_by($list, $field, $sortby = 'asc') { + if (is_array($list)) { + $refer = $resultSet = array(); + foreach ($list as $i => $data) { + $refer[$i] = &$data[$field]; + } + + switch ($sortby) { + case 'asc': // 正向排序 + asort($refer); + break; + case 'desc': // 逆向排序 + arsort($refer); + break; + case 'nat': // 自然排序 + natcasesort($refer); + break; + } + foreach ($refer as $key => $val) { + $resultSet[] = &$list[$key]; + } + + return $resultSet; + } + return false; +} + +/** + * 把返回的数据集转换成Tree + * @param array $list 要转换的数据集 + * @param string $pid parent标记字段 + * @param string $level level标记字段 + * @return array + * @author 麦当苗儿 + */ +function list_to_tree($list, $pk = 'id', $pid = 'pid', $child = '_child', $root = 0) { + // 创建Tree + $tree = array(); + if (is_array($list) && !is_object($list)) { + // 创建基于主键的数组引用 + $refer = array(); + foreach ($list as $key => $data) { + $refer[$data[$pk]] = &$list[$key]; + } + foreach ($list as $key => $data) { + // 判断是否存在parent + $parentId = $data[$pid]; + if ($root == $parentId) { + $tree[] = &$list[$key]; + } else { + if (isset($refer[$parentId])) { + $parent = &$refer[$parentId]; + $parent['childs'][] = $data['id']; + $parent[$child][] = &$list[$key]; + } + } + } + } + return $tree; +} + +/** + * 获取父树列表 + * + */ +function get_parent_tree($id = ''){ + if ($id) { + return array(); + } +} + + +/** + * 将list_to_tree的树还原成列表 + * @param array $tree 原来的树 + * @param string $child 孩子节点的键 + * @param string $order 排序显示的键,一般是主键 升序排列 + * @param array $list 过渡用的中间数组, + * @return array 返回排过序的列表数组 + * @author yangweijie + */ +function tree_to_list($tree, $child = '_child', $order = 'id', &$list = array()) { + if (is_array($tree)) { + foreach ($tree as $key => $value) { + $reffer = $value; + if (isset($reffer[$child])) { + unset($reffer[$child]); + tree_to_list($value[$child], $child, $order, $list); + } + $list[] = $reffer; + } + $list = list_sort_by($list, $order, $sortby = 'asc'); + } + return $list; +} + +// 分析枚举类型字段值 格式 a:名称1,b:名称2 +// 暂时和 parse_config_attr功能相同 +// 但请不要互相使用,后期会调整 +function parse_field_attr($string) { + if (0 === strpos($string, ':')) { + // 采用函数定义 + return eval('return ' . substr($string, 1) . ';'); + } elseif (0 === strpos($string, '[')) { + // 支持读取配置参数(必须是数组类型) + return \think\Config::get(substr($string, 1, -1)); + } + + $array = preg_split('/[,;\r\n]+/', trim($string, ",;\r\n")); + if (strpos($string, ':')) { + $value = array(); + foreach ($array as $val) { + list($k, $v) = explode(':', $val); + $value[$k] = $v; + } + } else { + $value = $array; + } + return $value; +} + +function parse_field_bind($table, $selected = '', $model = 0) { + $list = array(); + if ($table) { + $res = db($table)->select(); + foreach ($res as $key => $value) { + if (($model && isset($value['model_id']) && $value['model_id'] == $model) || (isset($value['model_id']) && $value['model_id'] == 0)) { + $list[] = $value; + } elseif(!$model) { + $list[] = $value; + } + } + if (!empty($list)) { + $tree = new \com\Tree(); + $list = $tree->toFormatTree($list); + } + } + return $list; +} + +// 分析枚举类型配置值 格式 a:名称1,b:名称2 +function parse_config_attr($string) { + $array = preg_split('/[,;\r\n]+/', trim($string, ",;\r\n")); + if (strpos($string, ':')) { + $value = array(); + foreach ($array as $val) { + list($k, $v) = explode(':', $val); + $value[$k] = $v; + } + } else { + $value = $array; + } + return $value; +} + +/** + * 记录行为日志,并执行该行为的规则 + * @param string $action 行为标识 + * @param string $model 触发行为的模型名 + * @param int $record_id 触发行为的记录id + * @param int $user_id 执行行为的用户id + * @return boolean + * @author huajie + */ +function action_log($action = null, $model = null, $record_id = null, $user_id = null) { + + //参数检查 + if (empty($action) || empty($model) || empty($record_id)) { + return '参数不能为空'; + } + if (empty($user_id)) { + $user_id = is_login(); + } + + //查询行为,判断是否执行 + $action_info = db('Action')->getByName($action); + if ($action_info['status'] != 1) { + return '该行为被禁用或删除'; + } + + //插入行为日志 + $data['action_id'] = $action_info['id']; + $data['user_id'] = $user_id; + $data['action_ip'] = ip2long(get_client_ip()); + $data['model'] = $model; + $data['record_id'] = $record_id; + $data['create_time'] = time(); + + //解析日志规则,生成日志备注 + if (!empty($action_info['log'])) { + if (preg_match_all('/\[(\S+?)\]/', $action_info['log'], $match)) { + $log['user'] = $user_id; + $log['record'] = $record_id; + $log['model'] = $model; + $log['time'] = time(); + $log['data'] = array('user' => $user_id, 'model' => $model, 'record' => $record_id, 'time' => time()); + foreach ($match[1] as $value) { + $param = explode('|', $value); + if (isset($param[1])) { + $replace[] = call_user_func($param[1], $log[$param[0]]); + } else { + $replace[] = $log[$param[0]]; + } + } + $data['remark'] = str_replace($match[0], $replace, $action_info['log']); + } else { + $data['remark'] = $action_info['log']; + } + } else { + //未定义日志规则,记录操作url + $data['remark'] = '操作url:' . $_SERVER['REQUEST_URI']; + } + + db('ActionLog')->insert($data); + + if (!empty($action_info['rule'])) { + //解析行为 + $rules = parse_action($action, $user_id); + + //执行行为 + $res = execute_action($rules, $action_info['id'], $user_id); + } +} + +/** + * 解析行为规则 + * 规则定义 table:$table|field:$field|condition:$condition|rule:$rule[|cycle:$cycle|max:$max][;......] + * 规则字段解释:table->要操作的数据表,不需要加表前缀; + * field->要操作的字段; + * condition->操作的条件,目前支持字符串,默认变量{$self}为执行行为的用户 + * rule->对字段进行的具体操作,目前支持四则混合运算,如:1+score*2/2-3 + * cycle->执行周期,单位(小时),表示$cycle小时内最多执行$max次 + * max->单个周期内的最大执行次数($cycle和$max必须同时定义,否则无效) + * 单个行为后可加 ; 连接其他规则 + * @param string $action 行为id或者name + * @param int $self 替换规则里的变量为执行用户的id + * @return boolean|array: false解析出错 , 成功返回规则数组 + * @author huajie + */ +function parse_action($action = null, $self) { + if (empty($action)) { + return false; + } + + //参数支持id或者name + if (is_numeric($action)) { + $map = array('id' => $action); + } else { + $map = array('name' => $action); + } + + //查询行为信息 + $info = db('Action')->where($map)->find(); + if (!$info || $info['status'] != 1) { + return false; + } + + //解析规则:table:$table|field:$field|condition:$condition|rule:$rule[|cycle:$cycle|max:$max][;......] + $rules = $info['rule']; + $rules = str_replace('{$self}', $self, $rules); + $rules = explode(';', $rules); + $return = array(); + foreach ($rules as $key => &$rule) { + $rule = explode('|', $rule); + foreach ($rule as $k => $fields) { + $field = empty($fields) ? array() : explode(':', $fields); + if (!empty($field)) { + $return[$key][$field[0]] = $field[1]; + } + } + //cycle(检查周期)和max(周期内最大执行次数)必须同时存在,否则去掉这两个条件 + if (!array_key_exists('cycle', $return[$key]) || !array_key_exists('max', $return[$key])) { + unset($return[$key]['cycle'], $return[$key]['max']); + } + } + + return $return; +} + +/** + * 执行行为 + * @param array $rules 解析后的规则数组 + * @param int $action_id 行为id + * @param array $user_id 执行的用户id + * @return boolean false 失败 , true 成功 + * @author huajie + */ +function execute_action($rules = false, $action_id = null, $user_id = null) { + if (!$rules || empty($action_id) || empty($user_id)) { + return false; + } + + $return = true; + foreach ($rules as $rule) { + + //检查执行周期 + $map = array('action_id' => $action_id, 'user_id' => $user_id); + $map['create_time'] = array('gt', time() - intval($rule['cycle']) * 3600); + $exec_count = db('ActionLog')->where($map)->count(); + if ($exec_count > $rule['max']) { + continue; + } + + //执行数据库操作 + $Model = db(ucfirst($rule['table'])); + $field = $rule['field']; + $res = $Model->where($rule['condition'])->setField($field, array('exp', $rule['rule'])); + + if (!$res) { + $return = false; + } + } + return $return; +} + +function avatar($uid, $size = 'middle') { + $size = in_array($size, array('big', 'middle', 'small', 'real')) ? $size : 'middle'; + $dir = setavatardir($uid); + $file = BASE_PATH . '/static/avatar/' . $dir . 'avatar_' . $size . '.png'; + if (!file_exists('.' . $file)) { + $file = BASE_PATH . '/static/images/default_avatar_' . $size . '.jpg'; + } + return $file; +} + +function setavatardir($uid) { + $uid = abs(intval($uid)); + $uid = sprintf("%09d", $uid); + $dir1 = substr($uid, 0, 3); + $dir2 = substr($uid, 3, 2); + $dir3 = substr($uid, 5, 2); + $dir4 = substr($uid, 7, 2); + $dir = $dir1 . '/' . $dir2 . '/' . $dir3 . '/' . $dir4 . '/'; + if (!is_dir("./uploads/avatar/$dir")) { + mk_dir("./uploads/avatar/" . $dir); + } + return $dir; +} + +function mk_dir($dir, $mode = 0755) { + if (is_dir($dir) || @mkdir($dir, $mode, true)) { + return true; + } + + if (!mk_dir(dirname($dir), $mode, true)) { + return false; + } + + return @mkdir($dir, $mode, true); +} + +/** + * 字符串转换为数组,主要用于把分隔符调整到第二个参数 + * @param string $str 要分割的字符串 + * @param string $glue 分割符 + * @return array + * @author 麦当苗儿 + */ +function str2arr($str = '', $glue = ',') { + if ($str) { + return explode($glue, $str); + } else { + return array(); + } +} + +/** + * 数组转换为字符串,主要用于把分隔符调整到第二个参数 + * @param array $arr 要连接的数组 + * @param string $glue 分割符 + * @return string + * @author 麦当苗儿 + */ +function arr2str($arr = array(), $glue = ',') { + if (empty($arr)) { + return ''; + } else { + return implode($glue, $arr); + } +} + +/** + * 格式化字节大小 + * @param number $size 字节数 + * @param string $delimiter 数字和单位分隔符 + * @return string 格式化后的带单位的大小 + * @author 麦当苗儿 + */ +function format_bytes($size, $delimiter = '') { + $units = array('B', 'KB', 'MB', 'GB', 'TB', 'PB'); + for ($i = 0; $size >= 1024 && $i < 5; $i++) { + $size /= 1024; + } + + return round($size, 2) . $delimiter . $units[$i]; +} + +function get_grid_list($list_grids) { + $grids = preg_split('/[;\r\n]+/s', trim($list_grids)); + foreach ($grids as &$value) { + // 字段:标题:链接 + $val = explode(':', $value); + // 支持多个字段显示 + $field = explode(',', $val[0]); + $value = array('field' => $field, 'title' => $val[1]); + if (isset($val[2])) { + // 链接信息 + $value['href'] = $val[2]; + // 搜索链接信息中的字段信息 + preg_replace_callback('/\[([a-z_]+)\]/', function ($match) use (&$fields) {$fields[] = $match[1];}, $value['href']); + } + if (strpos($val[1], '|')) { + // 显示格式定义 + list($value['title'], $value['format']) = explode('|', $val[1]); + } + foreach ($field as $val) { + $array = explode('|', $val); + $fields[] = $array[0]; + } + } + $data = array('grids' => $grids, 'fields' => $fields); + return $data; +} + +// 获取属性类型信息 +function get_attribute_type($type = '') { + // TODO 可以加入系统配置 + $type_array = config('config_type_list'); + static $type_list = array(); + foreach ($type_array as $key => $value) { + $type_list[$key] = explode(',', $value); + } + return $type ? $type_list[$type][0] : $type_list; +} + +//获得内容状态 +function get_content_status($status) { + $text = array( + '-1' => '删除', + '0' => '禁用', + '1' => '正常', + '2' => '待审核', + ); + return $text[$status]; +} + +/** + * 获取分类信息并缓存分类 + * @param integer $id 分类ID + * @param string $field 要获取的字段名 + * @return string 分类信息 + */ +function get_category($id, $field = null) { + /* 非法分类ID */ + if (empty($id) || !is_numeric($id)) { + return ''; + } + + $list = db('Category')->find($id); + return is_null($field) ? $list : $list[$field]; +} + +/* 根据ID获取分类标识 */ +function get_category_name($id) { + return get_category($id, 'title'); +} + +/* 根据ID获取分类名称 */ +function get_category_title($id) { + return get_category($id, 'title'); +} + +//分类分组 +function get_category_list_tree($model) { + $list = cache('sys_category_list'); + + /* 读取缓存数据 */ + if (empty($list)) { + $list = db('Category')->select(); + cache('sys_category_list', $list); + } + if ($model) { + foreach ($list as $key => $value) { + $models = explode(',', $value['model']); + if (in_array($model, $models)) { + $res[] = $value; + } + } + } else { + $res = $list; + } + + $tree = list_to_tree($res); + return $tree; +} + +//获取栏目子ID +function get_category_child($id) { + $list = cache('sys_category_list'); + + /* 读取缓存数据 */ + if (empty($list)) { + $list = db('category')->select(); + cache('sys_category_list', $list); + } + $ids[] = $id; + foreach ($list as $key => $value) { + if ($value['pid'] == $id) { + $ids[] = $value['id']; + $ids = array_merge($ids, get_category_child($value['id'])); + } + } + return array_unique($ids); +} + +/** + * 栏目文章当前位置 + * @param integer $id 当前栏目ID + * @param string $ext 文章标题 + * @return here 当前栏目树 + * @author K先森 <77413254@qq.com> + **/ +function get_category_pos($id,$ext=''){ + $cat = db('Category'); + $here = '首页'; + $uplevels = $cat->field("id,title,pid,model_id")->where("id=$id")->find(); + if(empty($uplevels)){ + return '栏目不存在'; + } + if($uplevels['pid'] != 0) + $here .= get_category_uplevels($uplevels['pid']); + $modelid = $uplevels['model_id']; + $modelname = db('Model')->field("name")->where("id = $modelid")->find(); + $links = url('index/content/lists?model='.$modelname['name'],array('id'=>$uplevels['id'])); + $here .= ' >> '.$uplevels['title'].""; + if($ext != '') $here .= ' >> '.$ext; + return $here; +} +function get_category_uplevels($id){ + $cat = db('Category'); + $here = ''; + $uplevels = $cat->field("id,title,pid,model_id")->where("id=$id")->find(); + $modelid = $uplevels['model_id']; + $modelname = db('Model')->field("name")->where("id = $modelid")->find(); + $links = url('index/content/lists?model='.$modelname['name'],array('id'=>$uplevels['id'])); + $here .= ' >> '.$uplevels['title'].""; + if($uplevels['pid'] != 0){ + $here = get_category_uplevels($uplevels['pid']).$here; + } + return $here; +} + + +function send_email($to, $subject, $message) { + $config = array( + 'protocol' => 'smtp', + 'smtp_host' => \think\Config::get('mail_host'), + 'smtp_user' => \think\Config::get('mail_username'), + 'smtp_pass' => \think\Config::get('mail_password'), + ); + $email = new \com\Email($config); + $email->from(\think\Config::get('mail_fromname'), \think\Config::get('web_site_title')); + $email->to($to); + + $email->subject($subject); + $email->message($message); + + return $email->send(); +} + +//实例化模型 +function M($name, $type = 'model') { + if ($type == 'model') { + return new \app\common\model\Content(strtolower($name)); + } elseif ($type == 'form') { + return new \app\common\model\DiyForm(strtolower($name)); + } +} + +//php获取中文字符拼音首字母 +function getFirstCharter($s0) { + $fchar = ord($s0{0}); + if ($fchar >= ord("A") and $fchar <= ord("z")) { + return strtoupper($s0{0}); + } + + $s1 = \iconv("UTF-8", "gb2312", $s0); + $s2 = \iconv("gb2312", "UTF-8", $s1); + if ($s2 == $s0) {$s = $s1;} else { $s = $s0;} + $asc = ord($s{0}) * 256 + ord($s{1}) - 65536; + if ($asc >= -20319 and $asc <= -20284) { + return "A"; + } + + if ($asc >= -20283 and $asc <= -19776) { + return "B"; + } + + if ($asc >= -19775 and $asc <= -19219) { + return "C"; + } + + if ($asc >= -19218 and $asc <= -18711) { + return "D"; + } + + if ($asc >= -18710 and $asc <= -18527) { + return "E"; + } + + if ($asc >= -18526 and $asc <= -18240) { + return "F"; + } + + if ($asc >= -18239 and $asc <= -17923) { + return "G"; + } + + if ($asc >= -17922 and $asc <= -17418) { + return "H"; + } + + if ($asc >= -17417 and $asc <= -16475) { + return "J"; + } + + if ($asc >= -16474 and $asc <= -16213) { + return "K"; + } + + if ($asc >= -16212 and $asc <= -15641) { + return "L"; + } + + if ($asc >= -15640 and $asc <= -15166) { + return "M"; + } + + if ($asc >= -15165 and $asc <= -14923) { + return "N"; + } + + if ($asc >= -14922 and $asc <= -14915) { + return "O"; + } + + if ($asc >= -14914 and $asc <= -14631) { + return "P"; + } + + if ($asc >= -14630 and $asc <= -14150) { + return "Q"; + } + + if ($asc >= -14149 and $asc <= -14091) { + return "R"; + } + + if ($asc >= -14090 and $asc <= -13319) { + return "S"; + } + + if ($asc >= -13318 and $asc <= -12839) { + return "T"; + } + + if ($asc >= -12838 and $asc <= -12557) { + return "W"; + } + + if ($asc >= -12556 and $asc <= -11848) { + return "X"; + } + + if ($asc >= -11847 and $asc <= -11056) { + return "Y"; + } + + if ($asc >= -11055 and $asc <= -10247) { + return "Z"; + } + + return null; +} + +function PyFirst($zh) { + $ret = ""; + $s1 = \iconv("UTF-8", "gb2312", $zh); + $s2 = \iconv("gb2312", "UTF-8", $s1); + if ($s2 == $zh) {$zh = $s1;} + for ($i = 0; $i < strlen($zh); $i++) { + $s1 = substr($zh, $i, 1); + $p = ord($s1); + if ($p > 160) { + $s2 = substr($zh, $i++, 2); + $ret .= getFirstCharter($s2); + } else { + $ret .= $s1; + } + } + return $ret; +} \ No newline at end of file diff --git a/app/controller/Admin.php b/app/controller/Admin.php index c6b0462f..ebeb882b 100644 --- a/app/controller/Admin.php +++ b/app/controller/Admin.php @@ -9,6 +9,7 @@ namespace app\controller; use app\BaseController; +use think\facade\Config; /** * @title 后端公共模块 @@ -21,6 +22,15 @@ class Admin extends BaseController { '\app\middleware\Admin' ]; - protected $data = ['meta' => [], 'data' => [], 'code' => 0, 'msg' => '']; + protected $data = ['meta' => ['title'=>''], 'data' => [], 'code' => 0, 'msg' => '']; + + protected function initialize(){ + $config = Config::get('system'); + if (!$config) { + $config = (new \app\model\Config())->lists(); + Config::set($config, 'system'); + } + $this->data['config'] = $config; + } } diff --git a/app/controller/admin/Config.php b/app/controller/admin/Config.php index d9dbdcc8..67df2107 100644 --- a/app/controller/admin/Config.php +++ b/app/controller/admin/Config.php @@ -9,6 +9,7 @@ namespace app\controller\admin; use app\controller\Admin; +use app\model\Config as ConfigModel; class Config extends Admin{ @@ -22,7 +23,21 @@ class Config extends Admin{ /** * @title 系统首页 */ - public function group(){ + public function group(ConfigModel $config){ + if ($this->request->isAjax()) { + # code... + }else{ + $this->data['id'] = $this->request->param('id', 1); + $res = $config->where(array('status' => 1, 'group' => $this->data['id']))->field('id,name,title,extra,value,remark,type')->order('sort')->select(); + $this->data['list'] = $res->toArray(); + return $this->data; + } + } + + /** + * @title 主题设置 + */ + public function themes(){ } } \ No newline at end of file diff --git a/app/controller/admin/Database.php b/app/controller/admin/Database.php index 6b8b10dd..55111d0d 100644 --- a/app/controller/admin/Database.php +++ b/app/controller/admin/Database.php @@ -13,9 +13,15 @@ use app\controller\Admin; class Database extends Admin{ /** - * @title 系统首页 + * @title 数据备份 */ - public function index(){ + public function export(){ + + } + /** + * @title 数据导入 + */ + public function import(){ } } \ No newline at end of file diff --git a/app/controller/admin/Index.php b/app/controller/admin/Index.php index b0d4b9a4..796ed609 100644 --- a/app/controller/admin/Index.php +++ b/app/controller/admin/Index.php @@ -31,11 +31,31 @@ class Index extends Admin{ /** * @title 后台登录 */ - public function login(){ + public function login(\app\model\Member $member){ if ($this->request->isAjax()) { - Session::set('user', ['uid'=>1,'username'=>'admin']); + $username = $this->request->param('username', ''); + $password = $this->request->param('password', ''); + $user = $member->login($username, $password); + if ($user) { + Session::set('user', $user); + $this->data['url'] = "/admin/index/index.html"; + $this->data['msg'] = "登录成功!"; + }else{ + Session::set('user', null); + $this->data['code'] = 1; + $this->data['msg'] = "登录失败!"; + } return $this->data; } } + + /** + * @title 后台登出 + */ + public function logout(){ + Session::set('user', null); + $this->data['code'] = 0; + return $this->data; + } } diff --git a/app/controller/admin/Seo.php b/app/controller/admin/Seo.php index 9743f8c8..d69a80de 100644 --- a/app/controller/admin/Seo.php +++ b/app/controller/admin/Seo.php @@ -18,4 +18,11 @@ class Seo extends Admin{ public function index(){ } + + /** + * @title 重写规则 + */ + public function rewrite(){ + + } } \ No newline at end of file diff --git a/app/middleware/Admin.php b/app/middleware/Admin.php index ff52fcea..8d456a92 100644 --- a/app/middleware/Admin.php +++ b/app/middleware/Admin.php @@ -19,11 +19,15 @@ class Admin { public function handle($request, \Closure $next) { $response = $next($request); - $this->data = $response->getData(); + if (is_array($response->getData())) { + $this->data = array_merge($this->data, $response->getData()); + } else { + $this->data = $response->getData(); + } if ($request->isAjax()) { return json($this->data); - }else{ + } else { return $response->data($this->fetch()); } } @@ -31,17 +35,18 @@ class Admin { /** * @title 显示类 */ - protected function fetch($template = ''){ + protected function fetch($template = '') { // 使用内置PHP模板引擎渲染模板输出 $config = array( - 'tpl_replace_string' => array( - '__static__' => '/static', - '__img__' => '/static/admin/images', - '__css__' => '/static/admin/css', - '__js__' => '/static/admin/js', - '__public__' => '/static/admin', - ) + 'tpl_replace_string' => array( + '__static__' => '/static', + '__img__' => '/static/admin/images', + '__css__' => '/static/admin/css', + '__js__' => '/static/admin/js', + '__public__' => '/static/admin', + ), ); + return View::config($config)->assign($this->data)->fetch($template); } } \ No newline at end of file diff --git a/app/middleware/AdminAuth.php b/app/middleware/AdminAuth.php index 8094570e..8d4c5019 100644 --- a/app/middleware/AdminAuth.php +++ b/app/middleware/AdminAuth.php @@ -8,21 +8,81 @@ // +---------------------------------------------------------------------- namespace app\middleware; -use think\facade\View; +use think\facade\Cache; use think\facade\Session; +use think\facade\View; /** * @title 后台中间件 */ class AdminAuth { + protected $data = ['headerMenu' => [], 'asideMenu' => []]; + public function handle($request, \Closure $next) { $user = Session::get('user'); - + if (Session::has('user') && $user['uid']) { + $request->user = $user; + if ($user['uid'] == 1) { + $request->isAdmin = true; + } + + $this->getMenu($request); //设置菜单 return $next($request); - }else{ + } else { return redirect(url('admin.index/login'))->remember(); } } + + /** + * @title 显示菜单 + */ + protected function getMenu($request) { + $current_controller = str_replace('.', '/', strtolower($request->controller())); + $current_url = $current_controller . '/' . strtolower($request->action()); + $menu = Cache::get('menu'); + if (!$menu) { + $dao = new \app\model\Menu(); + $res = $dao->where('is_menu', 1)->select(); + $menu = $res->toArray(); + Cache::set('menu', $menu); + } + $current_pid = 0; + foreach ($menu as $key => $value) { + if (strpos($value['url'], $current_controller) !== false) { + if ($value['pid'] == 0) { + $current_pid = $value['id']; + } else { + $current_pid = $value['pid']; + } + } + if ($request->isAdmin || in_array($value['id'], array())) { + $list[$value['id']] = $value; + } + } + $headerMenu = list_to_tree($list); + foreach ($headerMenu as $key => $value) { + if ($value['id'] == $current_pid) { + $value['active'] = true; + $current_aside = $value['_child']; + } else { + $value['active'] = false; + } + $headerMenu[$key] = $value; + } + + foreach ($current_aside as $key => $value) { + if ($current_url == $value['url']) { + $value['active'] = true; + } else { + $value['active'] = false; + } + $asideMenu[$value['group']][] = $value; + } + + $this->data['asideMenu'] = $asideMenu; + $this->data['headerMenu'] = $headerMenu; + View::assign($this->data); + } } \ No newline at end of file diff --git a/app/model/Config.php b/app/model/Config.php index 1f4a4f1a..8d98ad40 100644 --- a/app/model/Config.php +++ b/app/model/Config.php @@ -1,9 +1,102 @@ +// +---------------------------------------------------------------------- + namespace app\model; use think\Model; -class Config extends Model -{ +/** +* 设置模型 +*/ +class Config extends Model{ + + protected $type = array( + 'id' => 'integer', + ); + protected $auto = array('name', 'update_time', 'status'=>1); + protected $insert = array('create_time'); + + protected function setNameAttr($value){ + return strtolower($value); + } + + protected function getTypeTextAttr($value, $data){ + $type = config('config_type_list'); + $type_text = explode(',', $type[$data['type']]); + return $type_text[0]; + } + + public function lists(){ + $map = array('status' => 1); + $res = $this->where($map)->field('type,name,value')->select(); + + $config = array(); + foreach ($res->toArray() as $value) { + $config[$value['name']] = $this->parse($value['type'], $value['value']); + } + return $config; + } + + /** + * 根据配置类型解析配置 + * @param integer $type 配置类型 + * @param string $value 配置值 + * @author 麦当苗儿 + */ + private function parse($type, $value){ + switch ($type) { + case 'textarea': //解析数组 + $array = preg_split('/[,;\r\n]+/', trim($value, ",;\r\n")); + if(strpos($value,':')){ + $value = array(); + foreach ($array as $val) { + $list = explode(':', $val); + if(isset($list[2])){ + $value[$list[0]] = $list[1].','.$list[2]; + }else{ + $value[$list[0]] = $list[1]; + } + } + }else{ + $value = $array; + } + break; + } + return $value; + } + + public function getThemesList(){ + $files = array(); + $files['pc'] = $this->getList('pc'); + $files['mobile'] = $this->getList('mobile'); + return $files; + } + + protected function getList($type){ + $path = './template/'; + $file = opendir($path); + while (false !== ($filename = readdir($file))) { + if (!in_array($filename, array('.', '..'))) { + $files = $path . $filename . '/info.php'; + if (is_file($files)) { + $info = include($files); + if (isset($info['type']) && $info['type'] == $type) { + $info['id'] = $filename; + $info['img'] = '/template/' . $filename . '/' . $info['img']; + $list[] = $info; + }else{ + continue; + } + } + } + } + return isset($list) ? $list : array(); + } } \ No newline at end of file diff --git a/app/model/Member.php b/app/model/Member.php index ae381705..e01b0570 100644 --- a/app/model/Member.php +++ b/app/model/Member.php @@ -1,9 +1,125 @@ +// +---------------------------------------------------------------------- + namespace app\model; use think\Model; -class Member extends Model -{ +/** + * @title: 用户模型 + */ +class Member extends Model { + protected $pk = 'uid'; + + protected $createTime = 'reg_time'; + protected $updateTime = 'last_login_time'; + + protected $insert = ['reg_ip']; + + protected $type = [ + 'uid' => 'integer', + 'reg_time' => 'integer', + ]; + + public $loginVisible = array('uid', 'username', 'nickname', 'access_token'); //用户登录成功返回的字段 + + protected function setPasswordAttr($value) { + $salt = rand_string(6); + $this->set('salt', $salt); + return md5($value . $salt); + } + + protected function setRegIpAttr($value) { + return get_client_ip(1); + } + + protected function getAccessTokenAttr($value, $data) { + return authcode($data['uid'] . '|' . $data['username'] . '|' . $data['password'], 'ENCODE'); + } + + /** + * @title 用户登录 + * param username:用户名、手机号码、邮箱 password:密码、手机验证码、开源类型 type:登录类型(账号密码登录、手机号码+验证码登录、开源账号登录) + */ + public function login($username, $password, $type = 1) { + $map = array(); + if (preg_match("/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/", $username)) { + $type = 2; + } elseif (preg_match("/^1[34578]{1}\d{9}$/", $username)) { + $type = 3; + } + switch ($type) { + case 1: + $map['username'] = $username; + break; + case 2: + $map['email'] = $username; + break; + case 3: + $map['mobile'] = $username; + break; + default: + return false; //参数错误 + } + if (!$username) { + return false; + } + + $user = $this->where($map)->find(); + if (isset($user['uid']) && $user['uid'] && $user['status']) { + /* 验证用户密码 */ + if (md5($password . $user['salt']) === $user['password']) { + /* 更新登录信息 */ + (new MemberLog())->record($user); + return $user->append(array('access_token'))->visible($this->loginVisible)->toArray(); //登录成功,返回用户信息 + } else { + $this->error = "密码错误"; + return false; //密码错误 + } + } else { + $this->error = "用户不存在或被禁用"; + return false; + } + } + + /** + * @title: 注册 + */ + public function register($data) { + $result = self::create($data); + if (false !== $result) { + $user = $this->where('uid', $result->uid)->find(); + } else { + $this->error = "注册失败!"; + return false; + } + /* 更新登录信息 */ + (new MemberLog())->record($user); + return $user->append(array('access_token'))->visible($this->loginVisible)->toArray(); //登录成功,返回用户信息 + } + + /** + * @title: 获取用户列表 + */ + public function getUserList() { + + } + + /** + * @title: 获取用户信息 + */ + public function getInfo($uid) { + + } + + public function socialite() { + return $this->hasMany('MemberSocialite', 'uid', 'uid'); + } } \ No newline at end of file diff --git a/app/model/MemberLog.php b/app/model/MemberLog.php new file mode 100644 index 00000000..6b1c1fbb --- /dev/null +++ b/app/model/MemberLog.php @@ -0,0 +1,29 @@ + +// +---------------------------------------------------------------------- + +namespace app\model; + +use think\Model; + +/** + * @title: 用户日志模型 + */ +class MemberLog extends Model { + + public function record($user){ + /* 更新登录信息 */ + $data = array( + 'uid' => $user['uid'], + 'login' => array('inc', '1'), + 'last_login_time' => time(), + 'last_login_ip' => get_client_ip(1), + ); + Member::where(array('uid'=>$user['uid']))->update($data); + } +} \ No newline at end of file diff --git a/app/model/MemberSocialite.php b/app/model/MemberSocialite.php new file mode 100644 index 00000000..ab7bc6da --- /dev/null +++ b/app/model/MemberSocialite.php @@ -0,0 +1,59 @@ + +// +---------------------------------------------------------------------- + +namespace app\common\model; + +/** +* 用户模型 +*/ +class MemberSocialite extends Base{ + + protected $type = array( + 'uid' => 'integer', + ); + + public function register(){ + if ($socialite->where('openid', $data['info']['openid'])->value('id')) { + $this->error = "请勿重复注册!"; + return false; + } else { + $account = array( + 'username' => $data['info']['openid'], + 'password' => rand_string(8), + ); + $result = self::create($account); + if (false !== $result) { + $socialite_info = $data['info']; + $user = $this->where('uid', $result->uid)->find(); + $socialite_info['uid'] = $user['uid']; + $socialite_info['type'] = $data['open_type']; + $socialite->register($socialite_info); + } else { + $this->error = "注册失败!"; + return false; + } + } + } + + public function wechatLogin($user_info){ + $socialite = $this->where('openid', $user_info['openid'])->where('type', 'wechat')->find(); + $member = new Member(); + if ($socialite) { + $this->where('id', $socialite['id'])->update($user_info); + if ($socialite['uid'] > 0) { + //绑定了用户则自动登录 + $user = $member->where('uid', $socialite['uid'])->find(); + $member->autoLogin($user); + } + }else{ + $user_info['type'] = 'wechat'; + $this->insert($user_info); + } + } +} \ No newline at end of file diff --git a/app/model/Menu.php b/app/model/Menu.php new file mode 100644 index 00000000..eb91ba76 --- /dev/null +++ b/app/model/Menu.php @@ -0,0 +1,19 @@ + +// +---------------------------------------------------------------------- + +namespace app\model; + +use think\Model; + +/** + * @title: 菜单模型 + */ +class Menu extends Model { + +} \ No newline at end of file diff --git a/app/view/admin/base.html b/app/view/admin/base.html index 0179dab0..a07b029a 100644 --- a/app/view/admin/base.html +++ b/app/view/admin/base.html @@ -4,7 +4,7 @@ -SentCMS后台管理系统-{$meta|default="后台首页"} +SentCMS后台管理系统-{$meta['title']|default="后台首页"} @@ -73,49 +73,15 @@ @@ -192,8 +157,8 @@ 后台首页
  • - 后台首页 -
  • + 后台首页 + {include file="admin/setting"} - + - + + {block name="script"}{/block} \ No newline at end of file diff --git a/app/view/admin/channel/index.html b/app/view/admin/channel/index.html new file mode 100644 index 00000000..bdfbdaa3 --- /dev/null +++ b/app/view/admin/channel/index.html @@ -0,0 +1 @@ +{extend name="admin/base"/} \ No newline at end of file diff --git a/app/view/admin/config/group.html b/app/view/admin/config/group.html index bdfbdaa3..26f594c9 100644 --- a/app/view/admin/config/group.html +++ b/app/view/admin/config/group.html @@ -1 +1,95 @@ -{extend name="admin/base"/} \ No newline at end of file +{extend name="admin/base"/} +{block name="body"} + +
    +
    +
    +

    配置管理

    +
    + +
    +
    +
    + +
    +
    +
    + {volist name="list" id="item"} +
    + +
    + {switch name="item['type']"} + {case value="text"} + + {/case} + {case value="num"} + + {/case} + {case value="string"} + + {/case} + {case value="textarea"} + + {/case} + {case value="select"} + + {/case} + {case value="bool"} + + {/case} + {case value="image"} + {:widget('common/Form/showConfig',array($item,$item))} + {/case} + {/switch} + {if condition="$item['remark']"} + ({$item['remark']}) + {/if} +
    +
    + {/volist} +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +{/block} +{block name="script"} + +{/block} diff --git a/app/view/admin/config/themes.html b/app/view/admin/config/themes.html new file mode 100644 index 00000000..bdfbdaa3 --- /dev/null +++ b/app/view/admin/config/themes.html @@ -0,0 +1 @@ +{extend name="admin/base"/} \ No newline at end of file diff --git a/app/view/admin/database/export.html b/app/view/admin/database/export.html new file mode 100644 index 00000000..bdfbdaa3 --- /dev/null +++ b/app/view/admin/database/export.html @@ -0,0 +1 @@ +{extend name="admin/base"/} \ No newline at end of file diff --git a/app/view/admin/database/import.html b/app/view/admin/database/import.html new file mode 100644 index 00000000..bdfbdaa3 --- /dev/null +++ b/app/view/admin/database/import.html @@ -0,0 +1 @@ +{extend name="admin/base"/} \ No newline at end of file diff --git a/app/view/admin/index/clear.html b/app/view/admin/index/clear.html index bdfbdaa3..b6be4cb8 100644 --- a/app/view/admin/index/clear.html +++ b/app/view/admin/index/clear.html @@ -1 +1,15 @@ -{extend name="admin/base"/} \ No newline at end of file +{extend name="admin/base"/} + +{block name="body"} +
    +
    +
    +

    清除缓存

    +
    +
    +
    +
    +
    +
    +
    +{/block} \ No newline at end of file diff --git a/app/view/admin/menu/index.html b/app/view/admin/menu/index.html new file mode 100644 index 00000000..bdfbdaa3 --- /dev/null +++ b/app/view/admin/menu/index.html @@ -0,0 +1 @@ +{extend name="admin/base"/} \ No newline at end of file diff --git a/app/view/admin/model/index.html b/app/view/admin/model/index.html new file mode 100644 index 00000000..bdfbdaa3 --- /dev/null +++ b/app/view/admin/model/index.html @@ -0,0 +1 @@ +{extend name="admin/base"/} \ No newline at end of file diff --git a/app/view/admin/seo/index.html b/app/view/admin/seo/index.html new file mode 100644 index 00000000..bdfbdaa3 --- /dev/null +++ b/app/view/admin/seo/index.html @@ -0,0 +1 @@ +{extend name="admin/base"/} \ No newline at end of file diff --git a/app/view/admin/seo/rewrite.html b/app/view/admin/seo/rewrite.html new file mode 100644 index 00000000..bdfbdaa3 --- /dev/null +++ b/app/view/admin/seo/rewrite.html @@ -0,0 +1 @@ +{extend name="admin/base"/} \ No newline at end of file diff --git a/config/database.php b/config/database.php index a9a01e80..b2145810 100644 --- a/config/database.php +++ b/config/database.php @@ -46,7 +46,7 @@ return [ // 数据库编码默认采用utf8 'charset' => Env::get('database.charset', 'utf8'), // 数据库表前缀 - 'prefix' => Env::get('database.prefix', ''), + 'prefix' => Env::get('database.prefix', 'sent_'), // 数据库调试模式 'debug' => Env::get('database.debug', true), // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) diff --git a/public/static/admin/js/app.js b/public/static/admin/js/app.js index d6201869..ef59ec9a 100644 --- a/public/static/admin/js/app.js +++ b/public/static/admin/js/app.js @@ -137,9 +137,6 @@ $(function($) { $(this).addClass('active'); writeStorage(storage, 'config-skin', $(this).data('skin')); }); - - - setTimeout(function() { $('#content-wrapper > .row').css({ opacity: 1 @@ -200,7 +197,6 @@ $(function($) { $('#make-small-nav').click(function(e) { $('#page-wrapper').toggleClass('nav-small'); }); - setContentBody(); $(window).smartresize(function() { if ($(document).width() <= 991) { @@ -208,7 +204,6 @@ $(function($) { } setContentBody(); }); - $('.mobile-search').click(function(e) { e.preventDefault(); $('.mobile-search').addClass('active'); @@ -281,9 +276,9 @@ $.fn.removeClassPrefix = function(prefix) { return this; }; -function setContentBody(){ +function setContentBody() { header_height = $('header#header-navbar').height(); - if ($(window).height() - header_height -50 > $('#content-wrapper').height()) { - $('#content-wrapper').height($(window).height() - header_height -50); + if ($(window).height() - header_height - 50 > $('#content-wrapper').height()) { + $('#content-wrapper').height($(window).height() - header_height - 50); } } \ No newline at end of file diff --git a/public/static/admin/js/main.js b/public/static/admin/js/main.js new file mode 100644 index 00000000..0984eb14 --- /dev/null +++ b/public/static/admin/js/main.js @@ -0,0 +1,6 @@ +require.config({ + paths:{ + 'vue':'/static/js/vue.js', + "jquery":'/static/libs/jquery/jquery.min.js' + } +}); \ No newline at end of file diff --git a/public/static/js/require.js b/public/static/js/require.js new file mode 100644 index 00000000..a4203f0d --- /dev/null +++ b/public/static/js/require.js @@ -0,0 +1,5 @@ +/** vim: et:ts=4:sw=4:sts=4 + * @license RequireJS 2.3.6 Copyright jQuery Foundation and other contributors. + * Released under MIT license, https://github.com/requirejs/requirejs/blob/master/LICENSE + */ +var requirejs,require,define;!function(global,setTimeout){var req,s,head,baseElement,dataMain,src,interactiveScript,currentlyAddingScript,mainScript,subPath,version="2.3.6",commentRegExp=/\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/gm,cjsRequireRegExp=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,jsSuffixRegExp=/\.js$/,currDirRegExp=/^\.\//,op=Object.prototype,ostring=op.toString,hasOwn=op.hasOwnProperty,isBrowser=!("undefined"==typeof window||"undefined"==typeof navigator||!window.document),isWebWorker=!isBrowser&&"undefined"!=typeof importScripts,readyRegExp=isBrowser&&"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,defContextName="_",isOpera="undefined"!=typeof opera&&"[object Opera]"===opera.toString(),contexts={},cfg={},globalDefQueue=[],useInteractive=!1;function commentReplace(e,t){return t||""}function isFunction(e){return"[object Function]"===ostring.call(e)}function isArray(e){return"[object Array]"===ostring.call(e)}function each(e,t){var i;if(e)for(i=0;i= 0 && Math.floor(n) === n && isFinite(val) + } + + function isPromise (val) { + return ( + isDef(val) && + typeof val.then === 'function' && + typeof val.catch === 'function' + ) + } + + /** + * Convert a value to a string that is actually rendered. + */ + function toString (val) { + return val == null + ? '' + : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) + ? JSON.stringify(val, null, 2) + : String(val) + } + + /** + * Convert an input value to a number for persistence. + * If the conversion fails, return original string. + */ + function toNumber (val) { + var n = parseFloat(val); + return isNaN(n) ? val : n + } + + /** + * Make a map and return a function for checking if a key + * is in that map. + */ + function makeMap ( + str, + expectsLowerCase + ) { + var map = Object.create(null); + var list = str.split(','); + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase + ? function (val) { return map[val.toLowerCase()]; } + : function (val) { return map[val]; } + } + + /** + * Check if a tag is a built-in tag. + */ + var isBuiltInTag = makeMap('slot,component', true); + + /** + * Check if an attribute is a reserved attribute. + */ + var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); + + /** + * Remove an item from an array. + */ + function remove (arr, item) { + if (arr.length) { + var index = arr.indexOf(item); + if (index > -1) { + return arr.splice(index, 1) + } + } + } + + /** + * Check whether an object has the property. + */ + var hasOwnProperty = Object.prototype.hasOwnProperty; + function hasOwn (obj, key) { + return hasOwnProperty.call(obj, key) + } + + /** + * Create a cached version of a pure function. + */ + function cached (fn) { + var cache = Object.create(null); + return (function cachedFn (str) { + var hit = cache[str]; + return hit || (cache[str] = fn(str)) + }) + } + + /** + * Camelize a hyphen-delimited string. + */ + var camelizeRE = /-(\w)/g; + var camelize = cached(function (str) { + return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) + }); + + /** + * Capitalize a string. + */ + var capitalize = cached(function (str) { + return str.charAt(0).toUpperCase() + str.slice(1) + }); + + /** + * Hyphenate a camelCase string. + */ + var hyphenateRE = /\B([A-Z])/g; + var hyphenate = cached(function (str) { + return str.replace(hyphenateRE, '-$1').toLowerCase() + }); + + /** + * Simple bind polyfill for environments that do not support it, + * e.g., PhantomJS 1.x. Technically, we don't need this anymore + * since native bind is now performant enough in most browsers. + * But removing it would mean breaking code that was able to run in + * PhantomJS 1.x, so this must be kept for backward compatibility. + */ + + /* istanbul ignore next */ + function polyfillBind (fn, ctx) { + function boundFn (a) { + var l = arguments.length; + return l + ? l > 1 + ? fn.apply(ctx, arguments) + : fn.call(ctx, a) + : fn.call(ctx) + } + + boundFn._length = fn.length; + return boundFn + } + + function nativeBind (fn, ctx) { + return fn.bind(ctx) + } + + var bind = Function.prototype.bind + ? nativeBind + : polyfillBind; + + /** + * Convert an Array-like object to a real Array. + */ + function toArray (list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret + } + + /** + * Mix properties into target object. + */ + function extend (to, _from) { + for (var key in _from) { + to[key] = _from[key]; + } + return to + } + + /** + * Merge an Array of Objects into a single Object. + */ + function toObject (arr) { + var res = {}; + for (var i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]); + } + } + return res + } + + /* eslint-disable no-unused-vars */ + + /** + * Perform no operation. + * Stubbing args to make Flow happy without leaving useless transpiled code + * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/). + */ + function noop (a, b, c) {} + + /** + * Always return false. + */ + var no = function (a, b, c) { return false; }; + + /* eslint-enable no-unused-vars */ + + /** + * Return the same value. + */ + var identity = function (_) { return _; }; + + /** + * Generate a string containing static keys from compiler modules. + */ + function genStaticKeys (modules) { + return modules.reduce(function (keys, m) { + return keys.concat(m.staticKeys || []) + }, []).join(',') + } + + /** + * Check if two values are loosely equal - that is, + * if they are plain objects, do they have the same shape? + */ + function looseEqual (a, b) { + if (a === b) { return true } + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = Array.isArray(a); + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + return a.length === b.length && a.every(function (e, i) { + return looseEqual(e, b[i]) + }) + } else if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime() + } else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function (key) { + return looseEqual(a[key], b[key]) + }) + } else { + /* istanbul ignore next */ + return false + } + } catch (e) { + /* istanbul ignore next */ + return false + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b) + } else { + return false + } + } + + /** + * Return the first index at which a loosely equal value can be + * found in the array (if value is a plain object, the array must + * contain an object of the same shape), or -1 if it is not present. + */ + function looseIndexOf (arr, val) { + for (var i = 0; i < arr.length; i++) { + if (looseEqual(arr[i], val)) { return i } + } + return -1 + } + + /** + * Ensure a function is called only once. + */ + function once (fn) { + var called = false; + return function () { + if (!called) { + called = true; + fn.apply(this, arguments); + } + } + } + + var SSR_ATTR = 'data-server-rendered'; + + var ASSET_TYPES = [ + 'component', + 'directive', + 'filter' + ]; + + var LIFECYCLE_HOOKS = [ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'beforeDestroy', + 'destroyed', + 'activated', + 'deactivated', + 'errorCaptured', + 'serverPrefetch' + ]; + + /* */ + + + + var config = ({ + /** + * Option merge strategies (used in core/util/options) + */ + // $flow-disable-line + optionMergeStrategies: Object.create(null), + + /** + * Whether to suppress warnings. + */ + silent: false, + + /** + * Show production mode tip message on boot? + */ + productionTip: "development" !== 'production', + + /** + * Whether to enable devtools + */ + devtools: "development" !== 'production', + + /** + * Whether to record perf + */ + performance: false, + + /** + * Error handler for watcher errors + */ + errorHandler: null, + + /** + * Warn handler for watcher warns + */ + warnHandler: null, + + /** + * Ignore certain custom elements + */ + ignoredElements: [], + + /** + * Custom user key aliases for v-on + */ + // $flow-disable-line + keyCodes: Object.create(null), + + /** + * Check if a tag is reserved so that it cannot be registered as a + * component. This is platform-dependent and may be overwritten. + */ + isReservedTag: no, + + /** + * Check if an attribute is reserved so that it cannot be used as a component + * prop. This is platform-dependent and may be overwritten. + */ + isReservedAttr: no, + + /** + * Check if a tag is an unknown element. + * Platform-dependent. + */ + isUnknownElement: no, + + /** + * Get the namespace of an element + */ + getTagNamespace: noop, + + /** + * Parse the real tag name for the specific platform. + */ + parsePlatformTagName: identity, + + /** + * Check if an attribute must be bound using property, e.g. value + * Platform-dependent. + */ + mustUseProp: no, + + /** + * Perform updates asynchronously. Intended to be used by Vue Test Utils + * This will significantly reduce performance if set to false. + */ + async: true, + + /** + * Exposed for legacy reasons + */ + _lifecycleHooks: LIFECYCLE_HOOKS + }); + + /* */ + + /** + * unicode letters used for parsing html tags, component names and property paths. + * using https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname + * skipping \u10000-\uEFFFF due to it freezing up PhantomJS + */ + var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; + + /** + * Check if a string starts with $ or _ + */ + function isReserved (str) { + var c = (str + '').charCodeAt(0); + return c === 0x24 || c === 0x5F + } + + /** + * Define a property. + */ + function def (obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); + } + + /** + * Parse simple path. + */ + var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]")); + function parsePath (path) { + if (bailRE.test(path)) { + return + } + var segments = path.split('.'); + return function (obj) { + for (var i = 0; i < segments.length; i++) { + if (!obj) { return } + obj = obj[segments[i]]; + } + return obj + } + } + + /* */ + + // can we use __proto__? + var hasProto = '__proto__' in {}; + + // Browser environment sniffing + var inBrowser = typeof window !== 'undefined'; + var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; + var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); + var UA = inBrowser && window.navigator.userAgent.toLowerCase(); + var isIE = UA && /msie|trident/.test(UA); + var isIE9 = UA && UA.indexOf('msie 9.0') > 0; + var isEdge = UA && UA.indexOf('edge/') > 0; + var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); + var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); + var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; + var isPhantomJS = UA && /phantomjs/.test(UA); + var isFF = UA && UA.match(/firefox\/(\d+)/); + + // Firefox has a "watch" function on Object.prototype... + var nativeWatch = ({}).watch; + + var supportsPassive = false; + if (inBrowser) { + try { + var opts = {}; + Object.defineProperty(opts, 'passive', ({ + get: function get () { + /* istanbul ignore next */ + supportsPassive = true; + } + })); // https://github.com/facebook/flow/issues/285 + window.addEventListener('test-passive', null, opts); + } catch (e) {} + } + + // this needs to be lazy-evaled because vue may be required before + // vue-server-renderer can set VUE_ENV + var _isServer; + var isServerRendering = function () { + if (_isServer === undefined) { + /* istanbul ignore if */ + if (!inBrowser && !inWeex && typeof global !== 'undefined') { + // detect presence of vue-server-renderer and avoid + // Webpack shimming the process + _isServer = global['process'] && global['process'].env.VUE_ENV === 'server'; + } else { + _isServer = false; + } + } + return _isServer + }; + + // detect devtools + var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + + /* istanbul ignore next */ + function isNative (Ctor) { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) + } + + var hasSymbol = + typeof Symbol !== 'undefined' && isNative(Symbol) && + typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); + + var _Set; + /* istanbul ignore if */ // $flow-disable-line + if (typeof Set !== 'undefined' && isNative(Set)) { + // use native Set when available. + _Set = Set; + } else { + // a non-standard Set polyfill that only works with primitive keys. + _Set = /*@__PURE__*/(function () { + function Set () { + this.set = Object.create(null); + } + Set.prototype.has = function has (key) { + return this.set[key] === true + }; + Set.prototype.add = function add (key) { + this.set[key] = true; + }; + Set.prototype.clear = function clear () { + this.set = Object.create(null); + }; + + return Set; + }()); + } + + /* */ + + var warn = noop; + var tip = noop; + var generateComponentTrace = (noop); // work around flow check + var formatComponentName = (noop); + + { + var hasConsole = typeof console !== 'undefined'; + var classifyRE = /(?:^|[-_])(\w)/g; + var classify = function (str) { return str + .replace(classifyRE, function (c) { return c.toUpperCase(); }) + .replace(/[-_]/g, ''); }; + + warn = function (msg, vm) { + var trace = vm ? generateComponentTrace(vm) : ''; + + if (config.warnHandler) { + config.warnHandler.call(null, msg, vm, trace); + } else if (hasConsole && (!config.silent)) { + console.error(("[Vue warn]: " + msg + trace)); + } + }; + + tip = function (msg, vm) { + if (hasConsole && (!config.silent)) { + console.warn("[Vue tip]: " + msg + ( + vm ? generateComponentTrace(vm) : '' + )); + } + }; + + formatComponentName = function (vm, includeFile) { + if (vm.$root === vm) { + return '' + } + var options = typeof vm === 'function' && vm.cid != null + ? vm.options + : vm._isVue + ? vm.$options || vm.constructor.options + : vm; + var name = options.name || options._componentTag; + var file = options.__file; + if (!name && file) { + var match = file.match(/([^/\\]+)\.vue$/); + name = match && match[1]; + } + + return ( + (name ? ("<" + (classify(name)) + ">") : "") + + (file && includeFile !== false ? (" at " + file) : '') + ) + }; + + var repeat = function (str, n) { + var res = ''; + while (n) { + if (n % 2 === 1) { res += str; } + if (n > 1) { str += str; } + n >>= 1; + } + return res + }; + + generateComponentTrace = function (vm) { + if (vm._isVue && vm.$parent) { + var tree = []; + var currentRecursiveSequence = 0; + while (vm) { + if (tree.length > 0) { + var last = tree[tree.length - 1]; + if (last.constructor === vm.constructor) { + currentRecursiveSequence++; + vm = vm.$parent; + continue + } else if (currentRecursiveSequence > 0) { + tree[tree.length - 1] = [last, currentRecursiveSequence]; + currentRecursiveSequence = 0; + } + } + tree.push(vm); + vm = vm.$parent; + } + return '\n\nfound in\n\n' + tree + .map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) + ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") + : formatComponentName(vm))); }) + .join('\n') + } else { + return ("\n\n(found in " + (formatComponentName(vm)) + ")") + } + }; + } + + /* */ + + var uid = 0; + + /** + * A dep is an observable that can have multiple + * directives subscribing to it. + */ + var Dep = function Dep () { + this.id = uid++; + this.subs = []; + }; + + Dep.prototype.addSub = function addSub (sub) { + this.subs.push(sub); + }; + + Dep.prototype.removeSub = function removeSub (sub) { + remove(this.subs, sub); + }; + + Dep.prototype.depend = function depend () { + if (Dep.target) { + Dep.target.addDep(this); + } + }; + + Dep.prototype.notify = function notify () { + // stabilize the subscriber list first + var subs = this.subs.slice(); + if (!config.async) { + // subs aren't sorted in scheduler if not running async + // we need to sort them now to make sure they fire in correct + // order + subs.sort(function (a, b) { return a.id - b.id; }); + } + for (var i = 0, l = subs.length; i < l; i++) { + subs[i].update(); + } + }; + + // The current target watcher being evaluated. + // This is globally unique because only one watcher + // can be evaluated at a time. + Dep.target = null; + var targetStack = []; + + function pushTarget (target) { + targetStack.push(target); + Dep.target = target; + } + + function popTarget () { + targetStack.pop(); + Dep.target = targetStack[targetStack.length - 1]; + } + + /* */ + + var VNode = function VNode ( + tag, + data, + children, + text, + elm, + context, + componentOptions, + asyncFactory + ) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text; + this.elm = elm; + this.ns = undefined; + this.context = context; + this.fnContext = undefined; + this.fnOptions = undefined; + this.fnScopeId = undefined; + this.key = data && data.key; + this.componentOptions = componentOptions; + this.componentInstance = undefined; + this.parent = undefined; + this.raw = false; + this.isStatic = false; + this.isRootInsert = true; + this.isComment = false; + this.isCloned = false; + this.isOnce = false; + this.asyncFactory = asyncFactory; + this.asyncMeta = undefined; + this.isAsyncPlaceholder = false; + }; + + var prototypeAccessors = { child: { configurable: true } }; + + // DEPRECATED: alias for componentInstance for backwards compat. + /* istanbul ignore next */ + prototypeAccessors.child.get = function () { + return this.componentInstance + }; + + Object.defineProperties( VNode.prototype, prototypeAccessors ); + + var createEmptyVNode = function (text) { + if ( text === void 0 ) text = ''; + + var node = new VNode(); + node.text = text; + node.isComment = true; + return node + }; + + function createTextVNode (val) { + return new VNode(undefined, undefined, undefined, String(val)) + } + + // optimized shallow clone + // used for static nodes and slot nodes because they may be reused across + // multiple renders, cloning them avoids errors when DOM manipulations rely + // on their elm reference. + function cloneVNode (vnode) { + var cloned = new VNode( + vnode.tag, + vnode.data, + // #7975 + // clone children array to avoid mutating original in case of cloning + // a child. + vnode.children && vnode.children.slice(), + vnode.text, + vnode.elm, + vnode.context, + vnode.componentOptions, + vnode.asyncFactory + ); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.asyncMeta = vnode.asyncMeta; + cloned.isCloned = true; + return cloned + } + + /* + * not type checking this file because flow doesn't play well with + * dynamically accessing methods on Array prototype + */ + + var arrayProto = Array.prototype; + var arrayMethods = Object.create(arrayProto); + + var methodsToPatch = [ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' + ]; + + /** + * Intercept mutating methods and emit events + */ + methodsToPatch.forEach(function (method) { + // cache original method + var original = arrayProto[method]; + def(arrayMethods, method, function mutator () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + case 'unshift': + inserted = args; + break + case 'splice': + inserted = args.slice(2); + break + } + if (inserted) { ob.observeArray(inserted); } + // notify change + ob.dep.notify(); + return result + }); + }); + + /* */ + + var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + + /** + * In some cases we may want to disable observation inside a component's + * update computation. + */ + var shouldObserve = true; + + function toggleObserving (value) { + shouldObserve = value; + } + + /** + * Observer class that is attached to each observed + * object. Once attached, the observer converts the target + * object's property keys into getter/setters that + * collect dependencies and dispatch updates. + */ + var Observer = function Observer (value) { + this.value = value; + this.dep = new Dep(); + this.vmCount = 0; + def(value, '__ob__', this); + if (Array.isArray(value)) { + if (hasProto) { + protoAugment(value, arrayMethods); + } else { + copyAugment(value, arrayMethods, arrayKeys); + } + this.observeArray(value); + } else { + this.walk(value); + } + }; + + /** + * Walk through all properties and convert them into + * getter/setters. This method should only be called when + * value type is Object. + */ + Observer.prototype.walk = function walk (obj) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + defineReactive$$1(obj, keys[i]); + } + }; + + /** + * Observe a list of Array items. + */ + Observer.prototype.observeArray = function observeArray (items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } + }; + + // helpers + + /** + * Augment a target Object or Array by intercepting + * the prototype chain using __proto__ + */ + function protoAugment (target, src) { + /* eslint-disable no-proto */ + target.__proto__ = src; + /* eslint-enable no-proto */ + } + + /** + * Augment a target Object or Array by defining + * hidden properties. + */ + /* istanbul ignore next */ + function copyAugment (target, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target, key, src[key]); + } + } + + /** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + */ + function observe (value, asRootData) { + if (!isObject(value) || value instanceof VNode) { + return + } + var ob; + if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + ob = value.__ob__; + } else if ( + shouldObserve && + !isServerRendering() && + (Array.isArray(value) || isPlainObject(value)) && + Object.isExtensible(value) && + !value._isVue + ) { + ob = new Observer(value); + } + if (asRootData && ob) { + ob.vmCount++; + } + return ob + } + + /** + * Define a reactive property on an Object. + */ + function defineReactive$$1 ( + obj, + key, + val, + customSetter, + shallow + ) { + var dep = new Dep(); + + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return + } + + // cater for pre-defined getter/setters + var getter = property && property.get; + var setter = property && property.set; + if ((!getter || setter) && arguments.length === 2) { + val = obj[key]; + } + + var childOb = !shallow && observe(val); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter () { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + dep.depend(); + if (childOb) { + childOb.dep.depend(); + if (Array.isArray(value)) { + dependArray(value); + } + } + } + return value + }, + set: function reactiveSetter (newVal) { + var value = getter ? getter.call(obj) : val; + /* eslint-disable no-self-compare */ + if (newVal === value || (newVal !== newVal && value !== value)) { + return + } + /* eslint-enable no-self-compare */ + if (customSetter) { + customSetter(); + } + // #7981: for accessor properties without setter + if (getter && !setter) { return } + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + childOb = !shallow && observe(newVal); + dep.notify(); + } + }); + } + + /** + * Set a property on an object. Adds the new property and + * triggers change notification if the property doesn't + * already exist. + */ + function set (target, key, val) { + if (isUndef(target) || isPrimitive(target) + ) { + warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.length = Math.max(target.length, key); + target.splice(key, 1, val); + return val + } + if (key in target && !(key in Object.prototype)) { + target[key] = val; + return val + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + warn( + 'Avoid adding reactive properties to a Vue instance or its root $data ' + + 'at runtime - declare it upfront in the data option.' + ); + return val + } + if (!ob) { + target[key] = val; + return val + } + defineReactive$$1(ob.value, key, val); + ob.dep.notify(); + return val + } + + /** + * Delete a property and trigger change if necessary. + */ + function del (target, key) { + if (isUndef(target) || isPrimitive(target) + ) { + warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.splice(key, 1); + return + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + warn( + 'Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.' + ); + return + } + if (!hasOwn(target, key)) { + return + } + delete target[key]; + if (!ob) { + return + } + ob.dep.notify(); + } + + /** + * Collect dependencies on array elements when the array is touched, since + * we cannot intercept array element access like property getters. + */ + function dependArray (value) { + for (var e = (void 0), i = 0, l = value.length; i < l; i++) { + e = value[i]; + e && e.__ob__ && e.__ob__.dep.depend(); + if (Array.isArray(e)) { + dependArray(e); + } + } + } + + /* */ + + /** + * Option overwriting strategies are functions that handle + * how to merge a parent option value and a child option + * value into the final value. + */ + var strats = config.optionMergeStrategies; + + /** + * Options with restrictions + */ + { + strats.el = strats.propsData = function (parent, child, vm, key) { + if (!vm) { + warn( + "option \"" + key + "\" can only be used during instance " + + 'creation with the `new` keyword.' + ); + } + return defaultStrat(parent, child) + }; + } + + /** + * Helper that recursively merges two data objects together. + */ + function mergeData (to, from) { + if (!from) { return to } + var key, toVal, fromVal; + + var keys = hasSymbol + ? Reflect.ownKeys(from) + : Object.keys(from); + + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + // in case the object is already observed... + if (key === '__ob__') { continue } + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + set(to, key, fromVal); + } else if ( + toVal !== fromVal && + isPlainObject(toVal) && + isPlainObject(fromVal) + ) { + mergeData(toVal, fromVal); + } + } + return to + } + + /** + * Data + */ + function mergeDataOrFn ( + parentVal, + childVal, + vm + ) { + if (!vm) { + // in a Vue.extend merge, both should be functions + if (!childVal) { + return parentVal + } + if (!parentVal) { + return childVal + } + // when parentVal & childVal are both present, + // we need to return a function that returns the + // merged result of both functions... no need to + // check if parentVal is a function here because + // it has to be a function to pass previous merges. + return function mergedDataFn () { + return mergeData( + typeof childVal === 'function' ? childVal.call(this, this) : childVal, + typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal + ) + } + } else { + return function mergedInstanceDataFn () { + // instance merge + var instanceData = typeof childVal === 'function' + ? childVal.call(vm, vm) + : childVal; + var defaultData = typeof parentVal === 'function' + ? parentVal.call(vm, vm) + : parentVal; + if (instanceData) { + return mergeData(instanceData, defaultData) + } else { + return defaultData + } + } + } + } + + strats.data = function ( + parentVal, + childVal, + vm + ) { + if (!vm) { + if (childVal && typeof childVal !== 'function') { + warn( + 'The "data" option should be a function ' + + 'that returns a per-instance value in component ' + + 'definitions.', + vm + ); + + return parentVal + } + return mergeDataOrFn(parentVal, childVal) + } + + return mergeDataOrFn(parentVal, childVal, vm) + }; + + /** + * Hooks and props are merged as arrays. + */ + function mergeHook ( + parentVal, + childVal + ) { + var res = childVal + ? parentVal + ? parentVal.concat(childVal) + : Array.isArray(childVal) + ? childVal + : [childVal] + : parentVal; + return res + ? dedupeHooks(res) + : res + } + + function dedupeHooks (hooks) { + var res = []; + for (var i = 0; i < hooks.length; i++) { + if (res.indexOf(hooks[i]) === -1) { + res.push(hooks[i]); + } + } + return res + } + + LIFECYCLE_HOOKS.forEach(function (hook) { + strats[hook] = mergeHook; + }); + + /** + * Assets + * + * When a vm is present (instance creation), we need to do + * a three-way merge between constructor options, instance + * options and parent options. + */ + function mergeAssets ( + parentVal, + childVal, + vm, + key + ) { + var res = Object.create(parentVal || null); + if (childVal) { + assertObjectType(key, childVal, vm); + return extend(res, childVal) + } else { + return res + } + } + + ASSET_TYPES.forEach(function (type) { + strats[type + 's'] = mergeAssets; + }); + + /** + * Watchers. + * + * Watchers hashes should not overwrite one + * another, so we merge them as arrays. + */ + strats.watch = function ( + parentVal, + childVal, + vm, + key + ) { + // work around Firefox's Object.prototype.watch... + if (parentVal === nativeWatch) { parentVal = undefined; } + if (childVal === nativeWatch) { childVal = undefined; } + /* istanbul ignore if */ + if (!childVal) { return Object.create(parentVal || null) } + { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = {}; + extend(ret, parentVal); + for (var key$1 in childVal) { + var parent = ret[key$1]; + var child = childVal[key$1]; + if (parent && !Array.isArray(parent)) { + parent = [parent]; + } + ret[key$1] = parent + ? parent.concat(child) + : Array.isArray(child) ? child : [child]; + } + return ret + }; + + /** + * Other object hashes. + */ + strats.props = + strats.methods = + strats.inject = + strats.computed = function ( + parentVal, + childVal, + vm, + key + ) { + if (childVal && "development" !== 'production') { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = Object.create(null); + extend(ret, parentVal); + if (childVal) { extend(ret, childVal); } + return ret + }; + strats.provide = mergeDataOrFn; + + /** + * Default strategy. + */ + var defaultStrat = function (parentVal, childVal) { + return childVal === undefined + ? parentVal + : childVal + }; + + /** + * Validate component names + */ + function checkComponents (options) { + for (var key in options.components) { + validateComponentName(key); + } + } + + function validateComponentName (name) { + if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) { + warn( + 'Invalid component name: "' + name + '". Component names ' + + 'should conform to valid custom element name in html5 specification.' + ); + } + if (isBuiltInTag(name) || config.isReservedTag(name)) { + warn( + 'Do not use built-in or reserved HTML elements as component ' + + 'id: ' + name + ); + } + } + + /** + * Ensure all props option syntax are normalized into the + * Object-based format. + */ + function normalizeProps (options, vm) { + var props = options.props; + if (!props) { return } + var res = {}; + var i, val, name; + if (Array.isArray(props)) { + i = props.length; + while (i--) { + val = props[i]; + if (typeof val === 'string') { + name = camelize(val); + res[name] = { type: null }; + } else { + warn('props must be strings when using array syntax.'); + } + } + } else if (isPlainObject(props)) { + for (var key in props) { + val = props[key]; + name = camelize(key); + res[name] = isPlainObject(val) + ? val + : { type: val }; + } + } else { + warn( + "Invalid value for option \"props\": expected an Array or an Object, " + + "but got " + (toRawType(props)) + ".", + vm + ); + } + options.props = res; + } + + /** + * Normalize all injections into Object-based format + */ + function normalizeInject (options, vm) { + var inject = options.inject; + if (!inject) { return } + var normalized = options.inject = {}; + if (Array.isArray(inject)) { + for (var i = 0; i < inject.length; i++) { + normalized[inject[i]] = { from: inject[i] }; + } + } else if (isPlainObject(inject)) { + for (var key in inject) { + var val = inject[key]; + normalized[key] = isPlainObject(val) + ? extend({ from: key }, val) + : { from: val }; + } + } else { + warn( + "Invalid value for option \"inject\": expected an Array or an Object, " + + "but got " + (toRawType(inject)) + ".", + vm + ); + } + } + + /** + * Normalize raw function directives into object format. + */ + function normalizeDirectives (options) { + var dirs = options.directives; + if (dirs) { + for (var key in dirs) { + var def$$1 = dirs[key]; + if (typeof def$$1 === 'function') { + dirs[key] = { bind: def$$1, update: def$$1 }; + } + } + } + } + + function assertObjectType (name, value, vm) { + if (!isPlainObject(value)) { + warn( + "Invalid value for option \"" + name + "\": expected an Object, " + + "but got " + (toRawType(value)) + ".", + vm + ); + } + } + + /** + * Merge two option objects into a new one. + * Core utility used in both instantiation and inheritance. + */ + function mergeOptions ( + parent, + child, + vm + ) { + { + checkComponents(child); + } + + if (typeof child === 'function') { + child = child.options; + } + + normalizeProps(child, vm); + normalizeInject(child, vm); + normalizeDirectives(child); + + // Apply extends and mixins on the child options, + // but only if it is a raw options object that isn't + // the result of another mergeOptions call. + // Only merged options has the _base property. + if (!child._base) { + if (child.extends) { + parent = mergeOptions(parent, child.extends, vm); + } + if (child.mixins) { + for (var i = 0, l = child.mixins.length; i < l; i++) { + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + } + + var options = {}; + var key; + for (key in parent) { + mergeField(key); + } + for (key in child) { + if (!hasOwn(parent, key)) { + mergeField(key); + } + } + function mergeField (key) { + var strat = strats[key] || defaultStrat; + options[key] = strat(parent[key], child[key], vm, key); + } + return options + } + + /** + * Resolve an asset. + * This function is used because child instances need access + * to assets defined in its ancestor chain. + */ + function resolveAsset ( + options, + type, + id, + warnMissing + ) { + /* istanbul ignore if */ + if (typeof id !== 'string') { + return + } + var assets = options[type]; + // check local registration variations first + if (hasOwn(assets, id)) { return assets[id] } + var camelizedId = camelize(id); + if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } + var PascalCaseId = capitalize(camelizedId); + if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } + // fallback to prototype chain + var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; + if (warnMissing && !res) { + warn( + 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, + options + ); + } + return res + } + + /* */ + + + + function validateProp ( + key, + propOptions, + propsData, + vm + ) { + var prop = propOptions[key]; + var absent = !hasOwn(propsData, key); + var value = propsData[key]; + // boolean casting + var booleanIndex = getTypeIndex(Boolean, prop.type); + if (booleanIndex > -1) { + if (absent && !hasOwn(prop, 'default')) { + value = false; + } else if (value === '' || value === hyphenate(key)) { + // only cast empty string / same name to boolean if + // boolean has higher priority + var stringIndex = getTypeIndex(String, prop.type); + if (stringIndex < 0 || booleanIndex < stringIndex) { + value = true; + } + } + } + // check default value + if (value === undefined) { + value = getPropDefaultValue(vm, prop, key); + // since the default value is a fresh copy, + // make sure to observe it. + var prevShouldObserve = shouldObserve; + toggleObserving(true); + observe(value); + toggleObserving(prevShouldObserve); + } + { + assertProp(prop, key, value, vm, absent); + } + return value + } + + /** + * Get the default value of a prop. + */ + function getPropDefaultValue (vm, prop, key) { + // no default, return undefined + if (!hasOwn(prop, 'default')) { + return undefined + } + var def = prop.default; + // warn against non-factory defaults for Object & Array + if (isObject(def)) { + warn( + 'Invalid default value for prop "' + key + '": ' + + 'Props with type Object/Array must use a factory function ' + + 'to return the default value.', + vm + ); + } + // the raw prop value was also undefined from previous render, + // return previous default value to avoid unnecessary watcher trigger + if (vm && vm.$options.propsData && + vm.$options.propsData[key] === undefined && + vm._props[key] !== undefined + ) { + return vm._props[key] + } + // call factory function for non-Function types + // a value is Function if its prototype is function even across different execution context + return typeof def === 'function' && getType(prop.type) !== 'Function' + ? def.call(vm) + : def + } + + /** + * Assert whether a prop is valid. + */ + function assertProp ( + prop, + name, + value, + vm, + absent + ) { + if (prop.required && absent) { + warn( + 'Missing required prop: "' + name + '"', + vm + ); + return + } + if (value == null && !prop.required) { + return + } + var type = prop.type; + var valid = !type || type === true; + var expectedTypes = []; + if (type) { + if (!Array.isArray(type)) { + type = [type]; + } + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]); + expectedTypes.push(assertedType.expectedType || ''); + valid = assertedType.valid; + } + } + + if (!valid) { + warn( + getInvalidTypeMessage(name, value, expectedTypes), + vm + ); + return + } + var validator = prop.validator; + if (validator) { + if (!validator(value)) { + warn( + 'Invalid prop: custom validator check failed for prop "' + name + '".', + vm + ); + } + } + } + + var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; + + function assertType (value, type) { + var valid; + var expectedType = getType(type); + if (simpleCheckRE.test(expectedType)) { + var t = typeof value; + valid = t === expectedType.toLowerCase(); + // for primitive wrapper objects + if (!valid && t === 'object') { + valid = value instanceof type; + } + } else if (expectedType === 'Object') { + valid = isPlainObject(value); + } else if (expectedType === 'Array') { + valid = Array.isArray(value); + } else { + valid = value instanceof type; + } + return { + valid: valid, + expectedType: expectedType + } + } + + /** + * Use function string name to check built-in types, + * because a simple equality check will fail when running + * across different vms / iframes. + */ + function getType (fn) { + var match = fn && fn.toString().match(/^\s*function (\w+)/); + return match ? match[1] : '' + } + + function isSameType (a, b) { + return getType(a) === getType(b) + } + + function getTypeIndex (type, expectedTypes) { + if (!Array.isArray(expectedTypes)) { + return isSameType(expectedTypes, type) ? 0 : -1 + } + for (var i = 0, len = expectedTypes.length; i < len; i++) { + if (isSameType(expectedTypes[i], type)) { + return i + } + } + return -1 + } + + function getInvalidTypeMessage (name, value, expectedTypes) { + var message = "Invalid prop: type check failed for prop \"" + name + "\"." + + " Expected " + (expectedTypes.map(capitalize).join(', ')); + var expectedType = expectedTypes[0]; + var receivedType = toRawType(value); + var expectedValue = styleValue(value, expectedType); + var receivedValue = styleValue(value, receivedType); + // check if we need to specify expected value + if (expectedTypes.length === 1 && + isExplicable(expectedType) && + !isBoolean(expectedType, receivedType)) { + message += " with value " + expectedValue; + } + message += ", got " + receivedType + " "; + // check if we need to specify received value + if (isExplicable(receivedType)) { + message += "with value " + receivedValue + "."; + } + return message + } + + function styleValue (value, type) { + if (type === 'String') { + return ("\"" + value + "\"") + } else if (type === 'Number') { + return ("" + (Number(value))) + } else { + return ("" + value) + } + } + + function isExplicable (value) { + var explicitTypes = ['string', 'number', 'boolean']; + return explicitTypes.some(function (elem) { return value.toLowerCase() === elem; }) + } + + function isBoolean () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; }) + } + + /* */ + + function handleError (err, vm, info) { + // Deactivate deps tracking while processing error handler to avoid possible infinite rendering. + // See: https://github.com/vuejs/vuex/issues/1505 + pushTarget(); + try { + if (vm) { + var cur = vm; + while ((cur = cur.$parent)) { + var hooks = cur.$options.errorCaptured; + if (hooks) { + for (var i = 0; i < hooks.length; i++) { + try { + var capture = hooks[i].call(cur, err, vm, info) === false; + if (capture) { return } + } catch (e) { + globalHandleError(e, cur, 'errorCaptured hook'); + } + } + } + } + } + globalHandleError(err, vm, info); + } finally { + popTarget(); + } + } + + function invokeWithErrorHandling ( + handler, + context, + args, + vm, + info + ) { + var res; + try { + res = args ? handler.apply(context, args) : handler.call(context); + if (res && !res._isVue && isPromise(res) && !res._handled) { + res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); }); + // issue #9511 + // avoid catch triggering multiple times when nested calls + res._handled = true; + } + } catch (e) { + handleError(e, vm, info); + } + return res + } + + function globalHandleError (err, vm, info) { + if (config.errorHandler) { + try { + return config.errorHandler.call(null, err, vm, info) + } catch (e) { + // if the user intentionally throws the original error in the handler, + // do not log it twice + if (e !== err) { + logError(e, null, 'config.errorHandler'); + } + } + } + logError(err, vm, info); + } + + function logError (err, vm, info) { + { + warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); + } + /* istanbul ignore else */ + if ((inBrowser || inWeex) && typeof console !== 'undefined') { + console.error(err); + } else { + throw err + } + } + + /* */ + + var isUsingMicroTask = false; + + var callbacks = []; + var pending = false; + + function flushCallbacks () { + pending = false; + var copies = callbacks.slice(0); + callbacks.length = 0; + for (var i = 0; i < copies.length; i++) { + copies[i](); + } + } + + // Here we have async deferring wrappers using microtasks. + // In 2.5 we used (macro) tasks (in combination with microtasks). + // However, it has subtle problems when state is changed right before repaint + // (e.g. #6813, out-in transitions). + // Also, using (macro) tasks in event handler would cause some weird behaviors + // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109). + // So we now use microtasks everywhere, again. + // A major drawback of this tradeoff is that there are some scenarios + // where microtasks have too high a priority and fire in between supposedly + // sequential events (e.g. #4521, #6690, which have workarounds) + // or even between bubbling of the same event (#6566). + var timerFunc; + + // The nextTick behavior leverages the microtask queue, which can be accessed + // via either native Promise.then or MutationObserver. + // MutationObserver has wider support, however it is seriously bugged in + // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It + // completely stops working after triggering a few times... so, if native + // Promise is available, we will use it: + /* istanbul ignore next, $flow-disable-line */ + if (typeof Promise !== 'undefined' && isNative(Promise)) { + var p = Promise.resolve(); + timerFunc = function () { + p.then(flushCallbacks); + // In problematic UIWebViews, Promise.then doesn't completely break, but + // it can get stuck in a weird state where callbacks are pushed into the + // microtask queue but the queue isn't being flushed, until the browser + // needs to do some other work, e.g. handle a timer. Therefore we can + // "force" the microtask queue to be flushed by adding an empty timer. + if (isIOS) { setTimeout(noop); } + }; + isUsingMicroTask = true; + } else if (!isIE && typeof MutationObserver !== 'undefined' && ( + isNative(MutationObserver) || + // PhantomJS and iOS 7.x + MutationObserver.toString() === '[object MutationObserverConstructor]' + )) { + // Use MutationObserver where native Promise is not available, + // e.g. PhantomJS, iOS7, Android 4.4 + // (#6466 MutationObserver is unreliable in IE11) + var counter = 1; + var observer = new MutationObserver(flushCallbacks); + var textNode = document.createTextNode(String(counter)); + observer.observe(textNode, { + characterData: true + }); + timerFunc = function () { + counter = (counter + 1) % 2; + textNode.data = String(counter); + }; + isUsingMicroTask = true; + } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { + // Fallback to setImmediate. + // Techinically it leverages the (macro) task queue, + // but it is still a better choice than setTimeout. + timerFunc = function () { + setImmediate(flushCallbacks); + }; + } else { + // Fallback to setTimeout. + timerFunc = function () { + setTimeout(flushCallbacks, 0); + }; + } + + function nextTick (cb, ctx) { + var _resolve; + callbacks.push(function () { + if (cb) { + try { + cb.call(ctx); + } catch (e) { + handleError(e, ctx, 'nextTick'); + } + } else if (_resolve) { + _resolve(ctx); + } + }); + if (!pending) { + pending = true; + timerFunc(); + } + // $flow-disable-line + if (!cb && typeof Promise !== 'undefined') { + return new Promise(function (resolve) { + _resolve = resolve; + }) + } + } + + /* */ + + var mark; + var measure; + + { + var perf = inBrowser && window.performance; + /* istanbul ignore if */ + if ( + perf && + perf.mark && + perf.measure && + perf.clearMarks && + perf.clearMeasures + ) { + mark = function (tag) { return perf.mark(tag); }; + measure = function (name, startTag, endTag) { + perf.measure(name, startTag, endTag); + perf.clearMarks(startTag); + perf.clearMarks(endTag); + // perf.clearMeasures(name) + }; + } + } + + /* not type checking this file because flow doesn't play well with Proxy */ + + var initProxy; + + { + var allowedGlobals = makeMap( + 'Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + + 'require' // for Webpack/Browserify + ); + + var warnNonPresent = function (target, key) { + warn( + "Property or method \"" + key + "\" is not defined on the instance but " + + 'referenced during render. Make sure that this property is reactive, ' + + 'either in the data option, or for class-based components, by ' + + 'initializing the property. ' + + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', + target + ); + }; + + var warnReservedPrefix = function (target, key) { + warn( + "Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " + + 'properties starting with "$" or "_" are not proxied in the Vue instance to ' + + 'prevent conflicts with Vue internals' + + 'See: https://vuejs.org/v2/api/#data', + target + ); + }; + + var hasProxy = + typeof Proxy !== 'undefined' && isNative(Proxy); + + if (hasProxy) { + var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); + config.keyCodes = new Proxy(config.keyCodes, { + set: function set (target, key, value) { + if (isBuiltInModifier(key)) { + warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); + return false + } else { + target[key] = value; + return true + } + } + }); + } + + var hasHandler = { + has: function has (target, key) { + var has = key in target; + var isAllowed = allowedGlobals(key) || + (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)); + if (!has && !isAllowed) { + if (key in target.$data) { warnReservedPrefix(target, key); } + else { warnNonPresent(target, key); } + } + return has || !isAllowed + } + }; + + var getHandler = { + get: function get (target, key) { + if (typeof key === 'string' && !(key in target)) { + if (key in target.$data) { warnReservedPrefix(target, key); } + else { warnNonPresent(target, key); } + } + return target[key] + } + }; + + initProxy = function initProxy (vm) { + if (hasProxy) { + // determine which proxy handler to use + var options = vm.$options; + var handlers = options.render && options.render._withStripped + ? getHandler + : hasHandler; + vm._renderProxy = new Proxy(vm, handlers); + } else { + vm._renderProxy = vm; + } + }; + } + + /* */ + + var seenObjects = new _Set(); + + /** + * Recursively traverse an object to evoke all converted + * getters, so that every nested property inside the object + * is collected as a "deep" dependency. + */ + function traverse (val) { + _traverse(val, seenObjects); + seenObjects.clear(); + } + + function _traverse (val, seen) { + var i, keys; + var isA = Array.isArray(val); + if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { + return + } + if (val.__ob__) { + var depId = val.__ob__.dep.id; + if (seen.has(depId)) { + return + } + seen.add(depId); + } + if (isA) { + i = val.length; + while (i--) { _traverse(val[i], seen); } + } else { + keys = Object.keys(val); + i = keys.length; + while (i--) { _traverse(val[keys[i]], seen); } + } + } + + /* */ + + var normalizeEvent = cached(function (name) { + var passive = name.charAt(0) === '&'; + name = passive ? name.slice(1) : name; + var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first + name = once$$1 ? name.slice(1) : name; + var capture = name.charAt(0) === '!'; + name = capture ? name.slice(1) : name; + return { + name: name, + once: once$$1, + capture: capture, + passive: passive + } + }); + + function createFnInvoker (fns, vm) { + function invoker () { + var arguments$1 = arguments; + + var fns = invoker.fns; + if (Array.isArray(fns)) { + var cloned = fns.slice(); + for (var i = 0; i < cloned.length; i++) { + invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler"); + } + } else { + // return handler return value for single handlers + return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler") + } + } + invoker.fns = fns; + return invoker + } + + function updateListeners ( + on, + oldOn, + add, + remove$$1, + createOnceHandler, + vm + ) { + var name, def$$1, cur, old, event; + for (name in on) { + def$$1 = cur = on[name]; + old = oldOn[name]; + event = normalizeEvent(name); + if (isUndef(cur)) { + warn( + "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), + vm + ); + } else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on[name] = createFnInvoker(cur, vm); + } + if (isTrue(event.once)) { + cur = on[name] = createOnceHandler(event.name, cur, event.capture); + } + add(event.name, cur, event.capture, event.passive, event.params); + } else if (cur !== old) { + old.fns = cur; + on[name] = old; + } + } + for (name in oldOn) { + if (isUndef(on[name])) { + event = normalizeEvent(name); + remove$$1(event.name, oldOn[name], event.capture); + } + } + } + + /* */ + + function mergeVNodeHook (def, hookKey, hook) { + if (def instanceof VNode) { + def = def.data.hook || (def.data.hook = {}); + } + var invoker; + var oldHook = def[hookKey]; + + function wrappedHook () { + hook.apply(this, arguments); + // important: remove merged hook to ensure it's called only once + // and prevent memory leak + remove(invoker.fns, wrappedHook); + } + + if (isUndef(oldHook)) { + // no existing hook + invoker = createFnInvoker([wrappedHook]); + } else { + /* istanbul ignore if */ + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + // already a merged invoker + invoker = oldHook; + invoker.fns.push(wrappedHook); + } else { + // existing plain hook + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + + invoker.merged = true; + def[hookKey] = invoker; + } + + /* */ + + function extractPropsFromVNodeData ( + data, + Ctor, + tag + ) { + // we are only extracting raw values here. + // validation and default values are handled in the child + // component itself. + var propOptions = Ctor.options.props; + if (isUndef(propOptions)) { + return + } + var res = {}; + var attrs = data.attrs; + var props = data.props; + if (isDef(attrs) || isDef(props)) { + for (var key in propOptions) { + var altKey = hyphenate(key); + { + var keyInLowerCase = key.toLowerCase(); + if ( + key !== keyInLowerCase && + attrs && hasOwn(attrs, keyInLowerCase) + ) { + tip( + "Prop \"" + keyInLowerCase + "\" is passed to component " + + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + + " \"" + key + "\". " + + "Note that HTML attributes are case-insensitive and camelCased " + + "props need to use their kebab-case equivalents when using in-DOM " + + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." + ); + } + } + checkProp(res, props, key, altKey, true) || + checkProp(res, attrs, key, altKey, false); + } + } + return res + } + + function checkProp ( + res, + hash, + key, + altKey, + preserve + ) { + if (isDef(hash)) { + if (hasOwn(hash, key)) { + res[key] = hash[key]; + if (!preserve) { + delete hash[key]; + } + return true + } else if (hasOwn(hash, altKey)) { + res[key] = hash[altKey]; + if (!preserve) { + delete hash[altKey]; + } + return true + } + } + return false + } + + /* */ + + // The template compiler attempts to minimize the need for normalization by + // statically analyzing the template at compile time. + // + // For plain HTML markup, normalization can be completely skipped because the + // generated render function is guaranteed to return Array. There are + // two cases where extra normalization is needed: + + // 1. When the children contains components - because a functional component + // may return an Array instead of a single root. In this case, just a simple + // normalization is needed - if any child is an Array, we flatten the whole + // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep + // because functional components already normalize their own children. + function simpleNormalizeChildren (children) { + for (var i = 0; i < children.length; i++) { + if (Array.isArray(children[i])) { + return Array.prototype.concat.apply([], children) + } + } + return children + } + + // 2. When the children contains constructs that always generated nested Arrays, + // e.g.