* blog:xiaoz.me */ //载入通用函数 require("./functions/helper.php"); define("API_URL","https://onenav.xiaoz.top"); class Api { protected $db; public function __construct($db){ // 修改默认获取模式为关联数组 $db->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $this->db = $db; //返回json类型 header('Content-Type:application/json; charset=utf-8'); } /** * name:创建分类目录 */ public function add_category($token,$name,$property = 0,$weight = 0,$description = '',$font_icon = '',$fid = 0){ $this->auth($token); //分类名称不允许为空 if( empty($name) ) { $this->err_msg(-2000,'分类名称不能为空!'); } $data = [ 'name' => htmlspecialchars($name,ENT_QUOTES), 'add_time' => time(), 'weight' => $weight, 'property' => $property, 'description' => htmlspecialchars($description,ENT_QUOTES), 'font_icon' => $font_icon, 'fid' => $fid ]; //插入分类目录 $this->db->insert("on_categorys",$data); //返回ID $id = $this->db->id(); //如果id为空(NULL),说明插入失败了,姑且认为是name重复导致 if( empty($id) ){ $this->err_msg(-1000,'Categorie already exist!'); } else{ //成功并返回json格式 $data = [ 'code' => 0, 'id' => intval($id) ]; exit(json_encode($data)); } } /** * 修改分类目录 * */ public function edit_category($token,$id,$name,$property = 0,$weight = 0,$description = '',$font_icon = '',$fid = 0){ $this->auth($token); $fid = intval($fid); //如果id为空 if( empty($id) ){ $this->err_msg(-1003,'The category ID cannot be empty!'); } //根据fid查询这个分类是否存在 $count = $this->db->count("on_categorys", [ "id" => $fid ]); //如果fid不是0,且查询结果小于1,则认为这个父级ID是不存在的,则不允许修改 if( !empty($fid) && ($count < 1) ) { $this->err_msg(-2000,'父级ID不存在!'); } //查询fid是否是二级分类的ID,如果是,则不允许修改 $category = $this->db->get("on_categorys","*",[ "id" => $fid ]); //如果查询到他的父ID不是0,则是一个二级分类 if( intval($category['fid']) !== 0 ) { $this->err_msg(-2000,'父分类不能是二级分类!'); } //如果分类名为空 elseif( empty($name ) ){ $this->err_msg(-1004,'The category name cannot be empty!'); } //更新数据库 else{ //根据分类ID查询改分类下面是否已经存在子分类,如果存在子分类了则不允许设置为子分类,实用情况:一级分类下存在二级分类,无法再将改一级分类修改为二级分类 $count = $this->db->count("on_categorys", [ "fid" => $id ]); //该分类下的子分类数量大于0,并且父级ID修改为其它分类 if( ( $count > 0 ) && ( $fid !== 0 ) ) { $this->err_msg(-2000,'修改失败,该分类下已存在子分类!'); } $data = [ 'name' => htmlspecialchars($name,ENT_QUOTES), 'up_time' => time(), 'weight' => $weight, 'property' => $property, 'description' => htmlspecialchars($description,ENT_QUOTES), 'font_icon' => $font_icon, 'fid' => $fid ]; $re = $this->db->update('on_categorys',$data,[ 'id' => $id]); //var_dump( $this->db->log() ); //获取影响行数 $row = $re->rowCount(); if($row) { $data = [ 'code' => 0, 'msg' => 'successful' ]; exit(json_encode($data)); } else{ $this->err_msg(-1005,'The category name already exists!'); } } } /** * 删除分类目录 */ public function del_category($token,$id) { //验证授权 $this->auth($token); //如果id为空 if( empty($id) ){ $this->err_msg(-1003,'The category ID cannot be empty!'); } //如果分类目录下存在数据 $count = $this->db->count("on_links", [ "fid" => $id ]); //如果分类目录下存在数据,则不允许删除 if($count > 0) { $this->err_msg(-1006,'The category is not empty and cannot be deleted!'); } else{ $data = $this->db->delete('on_categorys',[ 'id' => $id] ); //返回影响行数 $row = $data->rowCount(); if($row) { $data = [ 'code' => 0, 'msg' => 'successful' ]; exit(json_encode($data)); } else{ $this->err_msg(-1007,'The category delete failed!'); } } } /** * name:返回错误(json) * */ public function err_msg($code,$err_msg){ $data = [ 'code' => $code, 'err_msg' => $err_msg ]; //返回json类型 header('Content-Type:application/json; charset=utf-8'); exit(json_encode($data)); } /** * name:验证方法 */ protected function auth($token){ // 当方法没有传递token的时候,则先尝试通过POST/GET获取token if( empty($token) ) { $token = empty( $_POST['token'] ) ? $_GET['token'] : $_POST['token']; } //计算正确的token:用户名 + TOKEN $SecretKey = @$this->db->get('on_options','*',[ 'key' => 'SecretKey' ])['value']; $token_yes = md5(USER.$SecretKey); //获取header中的X-token $xtoken = $_SERVER['HTTP_X_TOKEN']; //如果通过header传递token,且验证通过 if( !empty($xtoken) && ($xtoken === $token_yes) ) { return TRUE; } //如果token为空,则验证cookie if(empty($token)) { if( !$this->is_login() ) { $this->err_msg(-1002,'Authorization failure!'); } else if( $this->is_login() ){ return TRUE; } else{ $this->err_msg(-1002,'Cookie authorization failure!'); } } else if ( empty($SecretKey) ) { $this->err_msg(-2000,'请先生成SecretKey!'); } else if($token != $token_yes){ $this->err_msg(-1002,'Authorization failure!'); } else{ return TRUE; } } /** * name:添加链接 */ public function add_link($token,$fid,$title,$url,$description = '',$weight = 0,$property = 0,$url_standby = '',$font_icon = ''){ $this->auth($token); $fid = intval($fid); //检测链接是否合法 //$this->check_link($fid,$title,$url); $this->check_link([ 'fid' => $fid, 'title' => $title, 'url' => $url, 'url_standby' => $url_standby ]); //合并数据 $data = [ 'fid' => $fid, 'title' => htmlspecialchars($title,ENT_QUOTES), 'url' => $url, 'url_standby' => $url_standby, 'description' => htmlspecialchars($description,ENT_QUOTES), 'add_time' => time(), 'weight' => $weight, 'property' => $property ]; //如果$font_icon不为空,才一起追加写入数据库 if( !empty($font_icon) ) { $data['font_icon'] = $font_icon; } //插入数据库 $re = $this->db->insert('on_links',$data); //返回影响行数 $row = $re->rowCount(); //如果为真 if( $row ){ $id = $this->db->id(); $data = [ 'code' => 0, 'id' => $id ]; exit(json_encode($data)); } //如果插入失败 else{ $this->err_msg(-1011,'The URL already exists!'); } } /** * 批量修改链接分类 */ public function batch_modify_category($data) { $this->auth($token); //获取链接ID,是一个数组 $id = implode(',',$data['id']); //获取分类ID $fid = $data['fid']; //查询分类ID是否存在 $count = $this->db->count('on_categorys',[ 'id' => $fid]); //如果分类ID不存在 if( empty($fid) || empty($count) ) { $this->err_msg(-2000,'分类ID不存在!'); } else{ $sql = "UPDATE on_links SET fid='$fid' WHERE id IN ($id)"; $re = $this->db->query($sql); if( $re ) { $id = $this->db->id(); $data = [ 'code' => 0, 'msg' => "success" ]; exit(json_encode($data)); } else{ $this->err_msg(-2000,'更新失败!'); } } } /** * 批量修改链接属性为公有或私有 */ public function set_link_attribute($data) { $this->auth($token); //获取链接ID,是一个数组 $ids = implode(',',$data['ids']); $property = intval($data['property']); //拼接SQL文件 $sql = "UPDATE on_links SET property = $property WHERE id IN ($ids)"; $re = $this->db->query($sql); //返回影响行数 $row = $re->rowCount(); if ( $row > 0 ){ $this->return_json(200,"success"); } else{ $this->return_json(-2000,"failed"); } } /** * name:分类批量设置为私有或公有 * */ public function set_cat_batch($data) { $this->auth($token); //获取链接ID,是一个数组 $ids = implode(',',$data['ids']); $property = intval($data['property']); //拼接SQL文件 $sql = "UPDATE on_categorys SET property = $property WHERE id IN ($ids)"; // echo $sql; $re = $this->db->query($sql); //返回影响行数 $row = $re->rowCount(); if ( $row > 0 ){ $this->return_json(200,"success"); } else{ $this->return_json(-2000,"failed"); } } /** * 批量导入链接 */ public function imp_link($token,$filename,$fid,$property = 0){ //过滤$filename $filename = str_replace('../','',$filename); $filename = str_replace('./','',$filename); // 获取文件名称的后缀 $suffix = explode('.',$filename); // 如果没有后缀,则不合法,通过数组长度判断后缀 if( count($suffix) < 2 ) { $this->err_msg(-2000,'文件不合法!'); } // 获取文件后缀 $suffix = strtolower(end($suffix)); if( ( $suffix != 'html' ) && ( $suffix != 'htm' ) ) { $this->err_msg(-2000,'文件不合法!'); } $this->auth($token); //检查文件是否存在 if ( !file_exists($filename) ) { $this->err_msg(-1016,'File does not exist!'); } //解析HTML数据 $content = file_get_contents($filename); $pattern = "//i"; preg_match_all($pattern,$content,$arr); //失败次数 $fail = 0; //成功次数 $success = 0; //总数 $total = count($arr[0]); foreach( $arr[0] as $link ) { $pattern = "/http.*\"? ADD_DATE/i"; preg_match($pattern,$link,$urls); $url = str_replace('" ADD_DATE','',$urls[0]); $pattern = "/>.*<\/a>$/i"; preg_match($pattern,$link,$titles); $title = str_replace('>','',$titles[0]); $title = str_replace(' $fid, 'description' => '', 'add_time' => time(), 'weight' => 0, 'property' => $property ]; $data['title'] = $title; $data['url'] = $url; //插入数据库 $re = $this->db->insert('on_links',$data); //返回影响行数 $row = $re->rowCount(); //如果为真 if( $row ){ $id = $this->db->id(); $data = [ 'code' => 0, 'id' => $id ]; $success++; } //如果插入失败 else{ $fail++; } } //删除书签 unlink($filename); $data = [ 'code' => 0, 'msg' => '总数:'.$total.' 成功:'.$success.' 失败:'.$fail ]; exit(json_encode($data)); } /** * 批量导入链接并自动创建分类,这是新的导入接口 */ public function import_link($filename,$property = 0) { //过滤$filename $filename = str_replace('../','',$filename); $filename = str_replace('./','',$filename); $this->auth($token); //检查文件是否存在 if ( !file_exists($filename) ) { $this->err_msg(-1016,'File does not exist!'); } //解析HTML数据 $content = file_get_contents($filename); $HTMLs = explode("\n",$content);//分割文本 $data = []; //链接组 $categorys = []; //分类信息组 $categoryt = []; //分类信息表 // 遍历HTML foreach( $HTMLs as $HTMLh ){ //匹配分类名称 if( preg_match("/
(.*)<\/H3>/i",$HTMLh,$category) ){ //匹配到文件夹名时加入数组 array_push($categoryt,$category[1]); array_push($categorys,$category[1]); }elseif( preg_match('/<\/DL>

/i',$HTMLh) ){ //匹配到文件夹结束标记时删除一个 array_pop($categorys); }elseif( preg_match('/

(.+)<\/A>/i',$HTMLh,$urls) ){ $datat['category'] = $categorys[count($categorys) -1]; $datat['title'] = $urls[2]; $datat['url'] = $urls[1]; array_push($data,$datat); } } $categoryt = array_unique($categoryt); //追加一个默认分类,用来存储部分链接找不到分类的情况 array_push($categoryt,"默认分类"); //批量创建分类 $this->batch_create_category($categoryt); //查询所有分类 $categorys = $this->db->select("on_categorys",[ "name", "id", "fid" ]); // var_dump($categorys); // exit; //链接计数 $i = 0; //统计链接总数 $count = count($data); //批量导入链接 foreach ($data as $key => $value) { $category_name = trim($value['category']); //如果链接的分类是空的,则设置为默认分类 $category_name = empty( $category_name ) ? "默认分类" : $category_name; foreach ($categorys as $category) { if( trim( $category['name'] ) == $category_name ) { $fid = intval($category['id']); break; } } //合并数据 $link_data = [ 'fid' => $fid, 'title' => htmlspecialchars($value['title']), 'url' => htmlspecialchars($value['url'],ENT_QUOTES), 'add_time' => time(), 'weight' => 0, 'property' => $property ]; //插入数据库 $re = $this->db->insert('on_links',$link_data); //返回影响行数 $row = $re->rowCount(); if ($row) { $i++; } } //删除书签文件 unlink($filename); $this->return_json(200,"success",[ "count" => $count, "success" => $i, "failed" => $count - $i ]); } /** * 批量创建分类 * 接收一个一维数组 */ protected function batch_create_category($category_name) { $i = 0; foreach ($category_name as $key => $value) { $value = empty($value) ? "默认分类" : $value; $data = [ 'name' => trim($value), 'add_time' => time(), 'weight' => 0, 'property' => 1, 'description' => "书签导入时自动创建", 'fid' => 0 ]; try { //插入分类目录 $this->db->insert("on_categorys",$data); $i++; } catch (\Throwable $th) { continue; } } return $i; } /** * 书签上传 * type:上传类型,默认为上传书签,后续类型保留使用 */ public function upload($token,$type){ $this->auth($token); if ($_FILES["file"]["error"] > 0) { $this->err_msg(-1015,'File upload failed!'); } else { $filename = $_FILES["file"]["name"]; //获取文件后缀 $suffix = explode('.',$filename); $suffix = strtolower(end($suffix)); //临时文件位置 $temp = $_FILES["file"]["tmp_name"]; if( $suffix != 'html' ) { //删除临时文件 unlink($filename); $this->err_msg(-1014,'Unsupported file suffix name!'); } if( copy($temp,'data/'.$filename) ) { $data = [ 'code' => 0, 'file_name' => 'data/'.$filename ]; exit(json_encode($data)); } } } /** * name:通用上传接口 * @param1:指定上传路径 * @param2:指定允许的后缀名称,是一个数组 */ public function general_upload($path,$suffixs){ // 验证权限 $this->auth($token); // 存在错误,上传失败 if ($_FILES["file"]["error"] > 0) { $this->err_msg(-1015,'File upload failed!'); } else { $filename = $_FILES["file"]["name"]; //获取文件后缀 $suffix = explode('.',$filename); $suffix = strtolower(end($suffix)); //临时文件位置 $temp = $_FILES["file"]["tmp_name"]; // 遍历$suffixs后缀文件,判断是否允许 foreach ($suffixs as $key => $value) { if( $suffix == $value ) { $allow = true; break; } } // 如果是不允许的文件,则删除 if( $allow !== TRUE ) { //删除临时文件 unlink($filename); $this->err_msg(-1014,'Unsupported file suffix name!'); } // 如果是允许的文件,则移动到指定目录,path格式为data/ if( copy($temp,$path.$filename) ) { $data = [ 'code' => 0, 'file_name' => $path.$filename ]; exit(json_encode($data)); } else{ // 复制文件失败了 $this->err_msg(-2000,'上传失败,请检查' + $path + '目录权限!'); } } } /** * 图标上传 * type:上传类型 */ public function uploadImages($token){ $this->auth($token); //获取icon名称 $icon_name = $_POST['icon_name']; //获取老文件名称,然后删除 $old_pic = $_POST['old_pic']; //如果老文件名称合法,则删除 $pattern = "/^data\/upload\/[0-9]+\/[0-9a-zA-Z]+\.(jpg|jpeg|png|bmp|gif|svg)$/"; //如果名称不合法,则终止执行 if( preg_match($pattern,$old_pic) ){ @unlink($old_pic); } //如果名称是空的 if( empty($icon_name) ) { $this->return_json(-2000,'','获取图标名称失败!'); } if ($_FILES["file"]["error"] > 0) { //$this->err_msg(-1015,'File upload failed!'); $this->return_json(-2000,'','File upload failed!'); } else { //根据时间生成文件名 $filename = $_FILES["file"]["name"]; //获取文件后缀 $suffix = explode('.',$filename); $suffix = strtolower(end($suffix)); //临时文件位置 $temp = $_FILES["file"]["tmp_name"]; if( $suffix != 'ico' && $suffix != 'jpg' && $suffix != 'jpeg' && $suffix != 'png' && $suffix != 'bmp' && $suffix != 'gif' && $suffix != 'svg' ) { //删除临时文件 @unlink($filename); @unlink($temp); $this->return_json(-2000,'','Unsupported file suffix name!'); } //上传路径,格式为data/upload/202212/1669689755.png $upload_path = "data/upload/".date( "Ym", time() ).'/'.$icon_name.'.'.$suffix; //如果目录不存在,则创建 $upload_dir = dirname($upload_path); if( !is_dir( $upload_dir ) ) { //递归创建目录 mkdir($upload_dir,0755,true); } //$newfilename = 'upload/'.time().'.'.$suffix; //移动临时文件到指定上传路径 if( move_uploaded_file($temp,$upload_path) ) { $data = [ 'file_name' => $upload_path ]; $this->return_json(200,$data,'success'); } else{ $this->return_json(-2000,'','上传失败,请检查目录权限!'); } } } /** * 导出HTML链接进行备份 */ public function export_link(){ //鉴权 $this->auth($token); //查询所有分类 $categorys = $this->db->select("on_categorys","*"); //定义一个空数组用来存储查询后的数据 $data = []; //遍历分类 foreach ($categorys as $key => $category) { //查询该分类下的所有链接 $links = $this->db->select("on_links","*",[ "fid" => $category['id'] ]); // echo $category['name']; // var_dump($links); // exit; //组合为一个一维数组 $arr[$category['name']] = $links; // var_dump(); // exit; $data[$category['name']] = $arr[$category['name']]; //清除临时数据 unset($arr); } //返回数据 return $data; } /** * name:修改链接 */ public function edit_link($token,$id,$fid,$title,$url,$description = '',$weight = 0,$property = 0,$url_standby = '',$font_icon = ''){ $this->auth($token); $fid = intval($fid); /** * name:获取更新类型 * description:主要是因为兼容部分之前老的接口,老的接口不用变动,只能从OneNav后台添加图标,因此增加type判断是否是OneNav后台 * console:指从OneNav后台进行更新 */ $type = trim($_GET['type']); //检测链接是否合法 //$this->check_link($fid,$title,$url); $this->check_link([ 'fid' => $fid, 'title' => htmlspecialchars($title,ENT_QUOTES), 'url' => $url, 'url_standby' => $url_standby ]); //查询ID是否存在 $count = $this->db->count('on_links',[ 'id' => $id]); //如果id不存在 if( (empty($id)) || ($count == false) ) { $this->err_msg(-1012,'link id not exists!'); } //合并数据 $data = [ 'fid' => $fid, 'title' => htmlspecialchars($title,ENT_QUOTES), 'url' => $url, 'url_standby' => $url_standby, 'description' => htmlspecialchars($description,ENT_QUOTES), 'up_time' => time(), 'weight' => $weight, 'property' => $property ]; if( !empty($font_icon) ) { $data['font_icon'] = $font_icon; } //如果是从OneNav后台更新,则无论如何都要加上font_icon if( $type === 'console' ) { $data['font_icon'] = $font_icon; } //插入数据库 $re = $this->db->update('on_links',$data,[ 'id' => $id]); //返回影响行数 $row = $re->rowCount(); //如果为真 if( $row ){ $id = $this->db->id(); $data = [ 'code' => 0, 'msg' => 'successful' ]; exit(json_encode($data)); } //如果插入失败 else{ $this->err_msg(-1011,'The URL already exists!'); } } /** * name:单行链接修改 */ public function edit_link_row(){ //验证授权 $this->auth($token); // 获取POST请求中的JSON数据 $json_data = file_get_contents('php://input'); // 解析JSON数据为PHP对象 $obj = json_decode($json_data); $id = intval($obj->id); $fid = intval($obj->fid); //查询ID是否存在 $count = $this->db->count('on_links',[ 'id' => $id]); //如果id不存在 if( (empty($id)) || ($count == false) ) { $this->err_msg(-1012,'link id not exists!'); } // 拼接需要更新的数据 $data = [ 'title' => trim($obj->title), 'weight' => intval($obj->weight) ]; //插入数据库 $re = $this->db->update('on_links',$data,[ 'id' => $id]); //返回影响行数 $row = $re->rowCount(); //如果为真 if( $row ){ $id = $this->db->id(); $data = [ 'code' => 0, 'msg' => 'successful' ]; exit(json_encode($data)); } //如果插入失败 else{ $this->err_msg(-1011,'The URL already exists!'); } } /** * 删除链接 */ public function del_link($token,$id){ //验证token是否合法 $this->auth($token); //查询ID是否存在 $count = $this->db->count('on_links',[ 'id' => $id]); //如果id不存在 if( (empty($id)) || ($count == false) ) { $this->err_msg(-1010,'link id not exists!'); } else{ $re = $this->db->delete('on_links',[ 'id' => $id] ); if($re) { $data = [ 'code' => 0, 'msg' => 'successful' ]; exit(json_encode($data)); } else{ $this->err_msg(-1010,'link id not exists!'); } } } /** * 验证链接合法性 * 接收一个数组作为参数 */ protected function check_link($data){ $fid = $data['fid']; $title = $data['title']; $url = $data['url']; $url_standby = @$data['url_standby']; //如果父及(分类)ID不存在 if( empty($fid )) { $this->err_msg(-1007,'The category id(fid) not exist!'); } //如果父及ID不存在数据库中 //验证分类目录是否存在 $count = $this->db->count("on_categorys", [ "id" => $fid ]); if ( empty($count) ){ $this->err_msg(-1007,'The category not exist!'); } //如果链接标题为空 if( empty($title) ){ $this->err_msg(-1008,'The title cannot be empty!'); } //链接不能为空 if( empty($url) ){ $this->err_msg(-1009,'URL cannot be empty!'); } //通过正则匹配链接是否合法,支持http/https/ftp/magnet:?|ed2k|tcp/udp/thunder/rtsp/rtmp/sftp $pattern = "/^(http:\/\/|https:\/\/|ftp:\/\/|ftps:\/\/|magnet:?|ed2k:\/\/|tcp:\/\/|udp:\/\/|thunder:\/\/|rtsp:\/\/|rtmp:\/\/|sftp:\/\/).+/"; // if( !filter_var($url, FILTER_VALIDATE_URL) ) { // $this->err_msg(-1010,'URL is not valid!'); // } if ( !preg_match($pattern,$url) ) { $this->err_msg(-1010,'URL is not valid!'); } //备用链接不合法 if ( ( !empty($url_standby) ) && ( !preg_match($pattern, $url_standby) ) ) { $this->err_msg(-1010,'URL is not valid!'); } return true; } /** * 查询分类目录 */ public function category_list($page,$limit){ $token = @$_POST['token']; $offset = ($page - 1) * $limit; // 验证登录或 token if ($this->is_login() || (!empty($token) && $this->auth($token))) { $sql = "SELECT *,(SELECT name FROM on_categorys WHERE id = a.fid LIMIT 1) AS fname,(SELECT COUNT(id) FROM on_links WHERE fid = a.id) AS link_num FROM on_categorys as a ORDER BY fid ASC, weight DESC, id ASC"; $count = $this->db->count('on_categorys', '*'); } elseif (!empty($token)) { $this->auth($token); $sql = "SELECT *,(SELECT name FROM on_categorys WHERE id = a.fid LIMIT 1) AS fname,(SELECT COUNT(id) FROM on_links WHERE fid = a.id) AS link_num FROM on_categorys as a ORDER BY fid ASC, weight DESC, id ASC"; $count = $this->db->count('on_categorys', '*'); } else { $sql = "SELECT *,(SELECT name FROM on_categorys WHERE id = a.fid LIMIT 1) AS fname,(SELECT COUNT(id) FROM on_links WHERE fid = a.id) AS link_num FROM on_categorys as a WHERE property = 0 ORDER BY fid ASC, weight DESC, id ASC"; $count = $this->db->count('on_categorys', '*', ['property' => 0]); } $all_categories = $this->db->query($sql)->fetchAll(); // 构建排序规则 $sorted_categories = []; $categories_by_fid = []; foreach ($all_categories as $category) { if ($category['fid'] == 0) { // 父级分类 $sorted_categories[] = $category; } else { // 子分类,按 fid 分组 $categories_by_fid[$category['fid']][] = $category; } } // 对每个子分类组按 weight 排序 foreach ($categories_by_fid as &$sub_categories) { usort($sub_categories, function ($a, $b) { return $b['weight'] <=> $a['weight']; }); } // 将子分类插入到对应父级分类后 $final_sorted_categories = []; foreach ($sorted_categories as $parent_category) { $final_sorted_categories[] = $parent_category; if (isset($categories_by_fid[$parent_category['id']])) { $final_sorted_categories = array_merge($final_sorted_categories, $categories_by_fid[$parent_category['id']]); } } // 截取分页数据 $paged_data = array_slice($final_sorted_categories, $offset, $limit); $result = [ 'code' => 0, 'msg' => '', 'count' => $count, 'data' => $paged_data, ]; exit(json_encode($result)); } /** * 生成 */ public function create_sk() { //验证是否登录 $this->auth(''); $sk = md5(USER.USER.time()); $result = $this->set_option_bool('SecretKey',$sk); if( $result ){ $datas = [ 'code' => 0, 'data' => $sk ]; exit(json_encode($datas)); } else{ $this->err_msg(-2000,'SecretKey生成失败!'); } } /** * 查询链接 * 接收一个数组作为参数 */ public function link_list($data){ $limit = $data['limit']; $token = $data['token']; $offset = ($data['page'] - 1) * $data['limit']; //$fid = @$data['category_id']; $count = $this->db->count('on_links','*'); //如果成功登录,但token为空,获取所有 if( $this->is_login() || ( !empty($token) && $this->auth($token) ) ){ $sql = "SELECT *,(SELECT name FROM on_categorys WHERE id = on_links.fid) AS category_name FROM on_links ORDER BY weight DESC,id DESC LIMIT {$limit} OFFSET {$offset}"; } //如果token验证通过 elseif( (!empty($token)) && ($this->auth($token)) ) { $sql = "SELECT *,(SELECT name FROM on_categorys WHERE id = on_links.fid) AS category_name FROM on_links ORDER BY weight DESC,id DESC LIMIT {$limit} OFFSET {$offset}"; } //如果通过header传递的token验证成功,则获取所有 // else if( $this->auth("") === TRUE ) { // $sql = "SELECT *,(SELECT name FROM on_categorys WHERE id = on_links.fid) AS category_name FROM on_links ORDER BY weight DESC,id DESC LIMIT {$limit} OFFSET {$offset}"; // } //如果即没有登录成功,又没有token,则默认为游客,游客查询链接属性为公有,分类为公有,不查询私有 else{ $c_sql = "SELECT COUNT(*) AS num FROM on_links WHERE property = 0 AND fid IN (SELECT id FROM on_categorys WHERE property = 0)"; $count = $this->db->query($c_sql)->fetchAll()[0]['num']; $count = intval($count); $sql = "SELECT *,(SELECT name FROM on_categorys WHERE id = on_links.fid) AS category_name FROM on_links WHERE property = 0 AND fid IN (SELECT id FROM on_categorys WHERE property = 0) ORDER BY weight DESC,id DESC LIMIT {$limit} OFFSET {$offset}"; } //原生查询 $datas = $this->db->query($sql)->fetchAll(); $datas = [ 'code' => 0, 'msg' => '', 'count' => $count, 'data' => $datas ]; exit(json_encode($datas)); } /** * 查询某个分类下面的链接 * 接收一个数组作为参数 */ public function q_category_link($data){ $limit = $data['limit']; $token = $data['token']; $offset = ($data['page'] - 1) * $data['limit']; $fid = @$data['category_id']; //$fid = @$data['category_id']; $count = $this->db->count('on_links','*',[ 'fid' => $fid ]); //如果FID是空的,则直接终止 if( empty($fid) ) { $datas = [ 'code' => -2000, 'msg' => '分类ID不能为空!', 'count' => 0, 'data' => [] ]; exit(json_encode($datas)); } //如果成功登录,但token为空 if( ($this->is_login()) && (empty($token)) ){ $sql = "SELECT *,(SELECT name FROM on_categorys WHERE id = on_links.fid) AS category_name FROM on_links WHERE fid = $fid ORDER BY weight DESC,id DESC LIMIT {$limit} OFFSET {$offset}"; } //通过header获取token成功 // else if( $this->auth("") ) { // $sql = "SELECT *,(SELECT name FROM on_categorys WHERE id = on_links.fid) AS category_name FROM on_links WHERE fid = $fid ORDER BY weight DESC,id DESC LIMIT {$limit} OFFSET {$offset}"; // } //如果token验证通过 elseif( (!empty($token)) && ($this->auth($token)) ) { $sql = "SELECT *,(SELECT name FROM on_categorys WHERE id = on_links.fid) AS category_name FROM on_links WHERE fid = $fid ORDER BY weight DESC,id DESC LIMIT {$limit} OFFSET {$offset}"; } //如果即没有登录成功,又没有token,则默认为游客,游客查询链接属性为公有,分类为公有,不查询私有 else{ $c_sql = "SELECT COUNT(*) AS num FROM on_links WHERE property = 0 AND fid = $fid"; $count = $this->db->query($c_sql)->fetchAll()[0]['num']; $count = intval($count); $sql = "SELECT *,(SELECT name FROM on_categorys WHERE id = on_links.fid) AS category_name FROM on_links WHERE property = 0 AND fid = $fid ORDER BY weight DESC,id DESC LIMIT {$limit} OFFSET {$offset}"; } //原生查询 $datas = $this->db->query($sql)->fetchAll(); $datas = [ 'code' => 0, 'msg' => '', 'count' => $count, 'data' => $datas ]; exit(json_encode($datas)); } /** * 查询单个链接 * 此函数接收一个数组 */ public function get_a_link($data) { $id = $data['id']; $token = $data['token']; $link_info = $this->db->get("on_links","*",[ "id" => $id ]); //打印链接信息 //var_dump($link_info); //如果是公开链接,则直接返回 if ( $link_info['property'] == "0" ) { //链接是公开的,但是分类是私有的,则不显示 $category_property = $this->db->get("on_categorys","property",[ "id" => $link_info['fid'] ]); $category_property = intval($category_property); //分类属性为1,则说明是私有链接,则未认证用户不允许查询 if( $category_property === 1 ){ //进行认证 $this->auth($token); } $datas = [ 'code' => 0, 'data' => $link_info ]; } //如果是私有链接,并且认证通过 elseif( $link_info['property'] == "1" ) { if ( ( $this->auth($token) ) || ( $this->is_login() ) ) { $datas = [ 'code' => 0, 'data' => $link_info ]; } //exit(json_encode($datas)); } //如果是其它情况,则显示为空 else{ $datas = [ 'code' => 0, 'data' => [] ]; //exit(json_encode($datas)); } exit(json_encode($datas)); } /** * 查询单个分类信息 * 此函数接收一个数组 */ public function get_a_category($data) { $id = $data['id']; $token = $data['token']; $category_info = $this->db->get("on_categorys","*",[ "id" => $id ]); //var_dump($category_info); //如果是公开分类,则直接返回 if ( $category_info['property'] == "0" ) { $datas = [ 'code' => 0, 'data' => $category_info ]; } //如果是私有链接,并且认证通过 elseif( $category_info['property'] == "1" ) { if ( ( $this->auth($token) ) || ( $this->is_login() ) ) { $datas = [ 'code' => 0, 'data' => $category_info ]; } //exit(json_encode($datas)); } //如果是其它情况,则显示为空 else{ $datas = [ 'code' => 0, 'data' => [] ]; //exit(json_encode($datas)); } exit(json_encode($datas)); } /** * 验证是否登录 */ protected function is_login(){ $key = md5(USER.ENCRYPTED_PASSWORD.'onenav'.$_SERVER['HTTP_USER_AGENT']); //获取session $session = $_COOKIE['key']; //如果已经成功登录 if($session == $key) { return true; } else{ return false; } } /** * 获取链接信息 */ public function get_link_info($token,$url){ $this->auth($token); //检查链接是否合法 $pattern = "/^(http:\/\/|https:\/\/).*/"; //链接不合法 if( empty($url) ) { $this->err_msg(-2000,'URL不能为空!'); } if( !preg_match($pattern,$url) ){ $this->err_msg(-1010,'只支持识别http/https协议的链接!'); } else if( !filter_var($url, FILTER_VALIDATE_URL) ) { $this->err_msg(-2000,'只支持识别http/https协议的链接!'); } //获取网站标题 $c = curl_init(); curl_setopt($c, CURLOPT_URL, $url); curl_setopt($c, CURLOPT_RETURNTRANSFER, 1); curl_setopt($c, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($c, CURLOPT_SSL_VERIFYHOST, false); //设置超时时间 curl_setopt($c , CURLOPT_TIMEOUT, 10); $data = curl_exec($c); curl_close($c); $pos = strpos($data,'utf-8'); if($pos===false){$data = iconv("gbk","utf-8",$data);} preg_match("/(.*)<\/title>/i",$data, $title); $link['title'] = $title[1]; //获取网站描述 $tags = get_meta_tags($url); $link['description'] = $tags['description']; $data = [ 'code' => 0, 'data' => $link ]; exit(json_encode($data)); } /** * 自定义js */ public function add_js($token,$content){ $this->auth($token); //如果内容为空 // if( $content == '' ){ // $this->err_msg(-1013,'The content cannot be empty!'); // } //写入文件 try{ file_put_contents("data/extend.js",$content); $data = [ 'code' => 0, 'data' => 'success' ]; exit(json_encode($data)); } catch(Exception $e){ $this->err_msg(-2000,$e->getMessage()); } } /** * 获取IP */ //获取访客IP protected function getIP() { if (getenv('HTTP_CLIENT_IP')) { $ip = getenv('HTTP_CLIENT_IP'); } elseif (getenv('HTTP_X_FORWARDED_FOR')) { $ip = getenv('HTTP_X_FORWARDED_FOR'); } elseif (getenv('HTTP_X_FORWARDED')) { $ip = getenv('HTTP_X_FORWARDED'); } elseif (getenv('HTTP_FORWARDED_FOR')) { $ip = getenv('HTTP_FORWARDED_FOR'); } elseif (getenv('HTTP_FORWARDED')) { $ip = getenv('HTTP_FORWARDED'); } else { $ip = $_SERVER['REMOTE_ADDR']; } return $ip; } /** * name:检查弱密码 */ public function check_weak_password($token){ $this->auth($token); //如果用户名、密码为初始密码,则提示修改 if ( ( USER == 'xiaoz' ) && ( PASSWORD == 'xiaoz.me' ) ) { $this->err_msg(-1,'Weak password!'); } } /** * 获取SQL更新列表 * 循环读取db/sql/目录下的.sql文件 */ public function get_sql_update_list($data) { //鉴权 if( !$this->is_login() ) { $this->err_msg(-1002,'Authorization failure!'); } //待更新的数据库文件目录 $sql_dir = 'db/sql/'; //待更新的sql文件列表,默认为空 $sql_files_all = []; //打开一个目录,读取里面的文件列表 if (is_dir($sql_dir)){ if ($dh = opendir($sql_dir)){ while (($file = readdir($dh)) !== false){ //排除.和.. if ( ($file != ".") && ($file != "..") ) { array_push($sql_files_all,$file); } } //关闭句柄 closedir($dh); } } //判断数据库日志表是否存在 $sql = "SELECT count(*) AS num FROM sqlite_master WHERE type='table' AND name='on_db_logs'"; //查询结果 $q_result = $this->db->query($sql)->fetchAll(); //如果数量为0,则说明on_db_logs这个表不存在,需要提前导入 $num = intval($q_result[0]['num']); if ( $num === 0 ) { $data = [ "code" => 0, "data" => ['on_db_logs.sql'] ]; exit(json_encode($data)); }else{ //如果不为0,则需要查询数据库更新表里面的数据进行差集比对 $get_on_db_logs = $this->db->select("on_db_logs",[ "sql_name" ],[ "status" => "TRUE" ]); //声明一个空数组,存储已更新的数据库列表 $already_dbs = []; foreach ($get_on_db_logs as $value) { array_push($already_dbs,$value['sql_name']); } //array_diff() 函数返回两个数组的差集数组 $diff_result = array_diff($sql_files_all,$already_dbs); //去掉键 $diff_result = array_values($diff_result); sort($diff_result); $data = [ "code" => 0, "data" => $diff_result ]; exit(json_encode($data)); } } /** * 执行SQL更新语句,只执行单条更新 */ public function exe_sql($data) { //鉴权 if( !$this->is_login() ) { $this->err_msg(-1002,'Authorization failure!'); } //数据库sql目录 $sql_dir = 'db/sql/'; $name = $data['name']; //查询sql是否已经执行过 $count = $this->db->count("on_db_logs",[ "sql_name" => $name ]); if( $count >= 1 ) { $this->err_msg(-2000,$name."已经更新过!"); } $sql_name = $sql_dir.$name; //如果文件不存在,直接返回错误 if ( !file_exists($sql_name) ) { $this->err_msg(-2000,$name.'不存在!'); } //读取需要更新的SQL内容 try { //读取一个SQL文件,并将单个SQL文件拆分成单条SQL语句循环执行 switch ($name) { case '20220414.sql': $sql_content = explode("\n",file_get_contents($sql_name)); break; default: $sql_content = explode(';',file_get_contents($sql_name)); break; } //计算SQL总数 $num = count($sql_content) - 1; //初始数量设置为0 $init_num = 0; //遍历执行SQL语句 foreach ($sql_content as $sql) { //如果SQL为空,则跳过此次循环不执行 if( empty($sql) ) { continue; } $result = $this->db->query($sql); //只要单条SQL执行成功了就增加初始数量 if( $result ) { $init_num++; } } //无论最后结果如何,都将更新信息写入数据库 $insert_re = $this->db->insert("on_db_logs",[ "sql_name" => $name, "update_time" => time(), "status" => "TRUE" ]); if( $insert_re ) { $data = [ "code" => 0, "data" => $name."更新完成!总数${num},成功:${init_num}" ]; exit(json_encode($data)); } else { $this->err_msg(-2000,$name."更新失败,请人工检查!"); } } catch(Exception $e){ $this->err_msg(-2000,$e->getMessage()); } } /** * 保存主题参数 */ public function save_theme_config($data) { $this->auth($token); //获取主题名称 $name = $data['name']; //获取config参数,是一个对象 $config = $data['config']; //获取主题配置文件config.json if ( is_dir("templates/".$name) ) { $config_file = "templates/".$name."/config.json"; } else{ $config_file = "data/templates/".$name."/config.json"; } $config_content = json_encode($config,JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT); //写入配置 try { $re = @file_put_contents($config_file,$config_content); $this->return_json(0,"success"); } catch (\Throwable $th) { $this->err_msg(-2000,"写入配置失败!"); } } /** * 删除主题 */ public function delete_theme($name) { //验证授权 $this->auth($token); //正则判断主题名称是否合法 $pattern = "/^[a-zA-Z0-9][a-zA-Z0-9-_]+[a-zA-Z0-9]$/"; if ( !preg_match($pattern,$name) ) { $this->return_json(-2000,'',"主题名称不合法!"); } //如果是默认主题,则不允许删除 if( ($name === 'default') || ($name === 'admin') || ($name === 'default2') ) { $this->return_json(-2000,'',"默认主题不允许删除!"); } //查询当前使用中的主题 $current_theme = $this->db->get('on_options','value',[ 'key' => "theme" ]); //如果是当前使用中的主题也不允许删除 if ( $current_theme == $name ) { $this->return_json(-2000,'',"使用中的主题不允许删除!"); } //删除主题 $this->deldir("templates/".$name); $this->deldir("data/templates/".$name); //判断主题文件夹是否还存在 if( is_dir("templates/".$name) || is_dir("data/templates/".$name) ) { $this->return_json(-2000,'',"删除失败,可能是权限不足!"); } else{ $this->return_json(200,'',"主题已删除!"); } } /** * 删除一个目录 */ protected function deldir($dir) { //先删除目录下的文件: $dh=opendir($dir); while ($file=readdir($dh)) { if($file!="." && $file!="..") { $fullpath=$dir."/".$file; if(!is_dir($fullpath)) { unlink($fullpath); } else { $this->deldir($fullpath); } } } closedir($dh); //删除当前文件夹: if(rmdir($dir)) { return true; } else { return false; } } /** * 获取主题参数 */ public function get_theme_config() { $template = $this->db->get("on_options","value",[ "key" => "theme" ]); //获取主题配置信息 //获取主题配置 if( file_exists("templates/".$template."/config.json") ) { $config_file = "templates/".$template."/config.json"; } else if( file_exists("data/templates/".$template."/config.json") ) { $config_file = "data/templates/".$template."/config.json"; } else if( file_exists("templates/".$template."/info.json") ) { $config_file = "templates/".$template."/info.json"; } else { $config_file = "data/templates/".$template."/info.json"; } //读取主题配置 $config_content = @file_get_contents($config_file); //如果是info.json,则特殊处理下 if ( strstr($config_file,"info.json") ) { $config_content = json_decode($config_content); $theme_config = $config_content->config; } else{ $theme_config = $config_content; $theme_config = json_decode($theme_config); } $this->return_json(200,$theme_config,""); } /** * 通用json消息返回 */ public function return_json($code,$data,$msg = "") { $return = [ "code" => intval($code), "data" => $data, "msg" => $msg ]; exit(json_encode($return)); } /** * 更新option */ public function set_option($key,$value = '') { $key = htmlspecialchars(trim($key)); //如果key是空的 if( empty($key) ) { $this->err_msg(-2000,'键不能为空!'); } //鉴权 if( !$this->is_login() ) { $this->err_msg(-1002,'Authorization failure!'); } $count = $this->db->count("on_options", [ "key" => $key ]); //如果数量是0,则插入,否则就是更新 if( $count === 0 ) { try { $this->db->insert("on_options",[ "key" => $key, "value" => $value ]); $data = [ "code" => 0, "data" => "设置成功!" ]; exit(json_encode($data)); } catch (\Throwable $th) { $this->err_msg(-2000,$th); } } //更新数据 else if( $count === 1 ) { try { $this->db->update("on_options",[ "value" => $value ],[ "key" => $key ]); $data = [ "code" => 0, "data" => "设置已更新!" ]; exit(json_encode($data)); } catch (\Throwable $th) { $this->err_msg(-2000,$th); } } } /** * 更新option,返回BOOL值 */ protected function set_option_bool($key,$value = '') { $key = htmlspecialchars(trim($key)); //如果key是空的 if( empty($key) ) { return FALSE; } $count = $this->db->count("on_options", [ "key" => $key ]); //如果数量是0,则插入,否则就是更新 if( $count === 0 ) { try { $this->db->insert("on_options",[ "key" => $key, "value" => $value ]); $data = [ "code" => 0, "data" => "设置成功!" ]; return TRUE; } catch (\Throwable $th) { return FALSE; } } //更新数据 else if( $count === 1 ) { try { $this->db->update("on_options",[ "value" => $value ],[ "key" => $key ]); $data = [ "code" => 0, "data" => "设置已更新!" ]; return TRUE; } catch (\Throwable $th) { return FALSE; } } } /** * 用户状态 */ public function check_login($token){ $re = $this->auth($token); if( $re ) { $this->return_json(200,"true","success"); } } /** * 验证订阅是否有效 */ public function check_subscribe() { //验证token是否合法 $this->auth($token); //获取订阅信息 //获取当前站点信息 $subscribe = $this->db->get('on_options','value',[ 'key' => "s_subscribe" ]); $domain = $_SERVER['HTTP_HOST']; $subscribe = unserialize($subscribe); //api请求地址 $api_url = API_URL."/v1/check_subscribe.php?order_id=".$subscribe['order_id']."&email=".$subscribe['email']."&domain=".$domain; // 如果邮箱或者订单号为空,则返回提示 if( empty($subscribe['order_id']) || empty($subscribe['email']) ) { $this->return_json(-2000,'','此功能需要订阅!'); } try { #GET HTTPS $curl = curl_init($api_url); #设置useragent curl_setopt($curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36"); curl_setopt($curl, CURLOPT_FAILONERROR, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); #设置超时时间,最小为1s(可选) curl_setopt($curl , CURLOPT_TIMEOUT, 30); $html = curl_exec($curl); curl_close($curl); //解析json $data = json_decode($html); //var_dump($data->data->end_time); //echo strtotime($data->data->end_time); //var_dump($data->code); //如果状态码返回200,并且订阅没有到期 if( (intval($data->code) === 200) && ( $data->data->end_time > ( strtotime( date("Y-m-d",time()) ) )) ) { $this->return_json(200,$data->data,'success'); } else if( intval($data->code === -1000 ) ) { $this->return_json(-2000,'',$data->msg); } else{ $this->return_json(-2000,'',"请求接口失败,请重试!"); } } catch (\Throwable $th) { $this->return_json(-2000,'','网络请求失败,请重试!'); } } /** * 下载主题 */ public function down_theme($data) { //主题名称 $name = $data['name']; //key-value $key = $data['key']; $value = $data['value']; //拼接主题URL $url = API_URL."/v1/down_theme.php?name=${name}&key=${key}&value=${value}"; //验证token是否合法 $this->auth($token); //检查主题是否已经存在 if ( $data['type'] == 'download' ) { $theme1 = "templates/".$name; $theme2 = "data/templates/".$name; if( is_dir($theme1) || is_dir($theme2) ) { $this->return_json(-2000,'','主题已存在,无需重复下载!'); } } //如果返回404状态 $res = get_headers($url,1); if( strstr($res[0],'404') ) { $this->return_json(-2000,'','远程服务器上不存在此主题!'); } //判断主题目录是否存在,如果curl_host是alpine,则视为容器,容器则将主题目录设置为data/templates $curl_host = curl_version()['host']; if( strstr($curl_host,'alpine') ) { // 默认主题一律保存到templates目录 if( $name == "default2" ) { $theme_dir = "templates"; } else{ $theme_dir = "data/templates"; } } else{ $theme_dir = "templates"; } //主题完整压缩包路径 $file_name = $theme_dir."/${name}.tar.gz"; if( !is_dir($theme_dir) ) { mkdir($theme_dir,0755); } //尝试下载主题 try { //下载主题,并设置超时时间为120s $content = $this->curl_get($url,120); //写入主题 $re = file_put_contents($theme_dir."/${name}.tar.gz",$content); //如果写入主题失败了,说明权限不粗糙 if( !$re ) { $this->return_json(-2000,'','主题写入失败,请检查目录权限!'); } else{ //解压文件 $phar = new PharData($file_name); //路径 要解压的文件 是否覆盖 $phar->extractTo($theme_dir."/${name}", null, true); //删除主题 unlink($file_name); $this->return_json(200,'','主题下载成功!'); } } catch (\Throwable $th) { $this->return_json(-2000,'','主题下载失败,请检查目录权限!'); } finally{ unlink($file_name); } } /** * 验证订阅是否存在 */ public function is_subscribe() { //获取订阅SESSION状态 session_start(); //获取session订阅状态 $is_subscribe = $_SESSION['subscribe']; //如果订阅是空的,则请求接口获取订阅状态 if ( !isset($is_subscribe) ) { //获取当前站点信息 $subscribe = $this->db->get('on_options','value',[ 'key' => "s_subscribe" ]); $domain = $_SERVER['HTTP_HOST']; $subscribe = unserialize($subscribe); //api请求地址 $api_url = API_URL."/v1/check_subscribe.php?order_id=".$subscribe['order_id']."&email=".$subscribe['email']."&domain=".$domain; // echo $api_url; try { #GET HTTPS $curl = curl_init($api_url); #设置useragent curl_setopt($curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36"); curl_setopt($curl, CURLOPT_FAILONERROR, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); #设置超时时间,最小为1s(可选) curl_setopt($curl , CURLOPT_TIMEOUT, 30); $html = curl_exec($curl); curl_close($curl); //解析json $data = json_decode($html); //var_dump($data->data->end_time); //echo strtotime($data->data->end_time); //var_dump($data->code); //如果状态码返回200,并且订阅没有到期 if( (intval($data->code) === 200) && ( $data->data->end_time > ( strtotime( date("Y-m-d",time()) ) )) ) { $_SESSION['subscribe'] = TRUE; return TRUE; } else if( intval($data->code === -1000 ) ) { $_SESSION['subscribe'] = FALSE; return FALSE; } else{ $_SESSION['subscribe'] = NULL; } } catch (\Throwable $th) { $_SESSION['subscribe'] = NULL; } } if( $is_subscribe == TRUE ) { return TRUE; } else{ return FALSE; } } /** * name:验证订阅,订阅不存在,则阻止 */ public function check_is_subscribe(){ $result = $this->is_subscribe(); if( $result === FALSE ) { $this->return_json(-2000,'','该功能需要订阅后才能使用!'); } else if( $result === TRUE ) { return TRUE; } else{ $this->return_json(-2000,'','该功能需要订阅后才能使用!'); } } /** * 无脑下载更新程序 */ public function down_updater() { $url = API_URL."/update.tar.gz"; // echo $url; // exit; try { //检查本地是否存在更新程序 $curl = curl_init($url); #设置useragent curl_setopt($curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36"); curl_setopt($curl, CURLOPT_FAILONERROR, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); #设置超时时间,最小为1s(可选) curl_setopt($curl , CURLOPT_TIMEOUT, 60); $html = curl_exec($curl); curl_close($curl); //var_dump($html); //return $html; //写入文件 file_put_contents("update.tar.gz",$html); //解压覆盖文件 //解压文件 $phar = new PharData('update.tar.gz'); //路径 要解压的文件 是否覆盖 $phar->extractTo('./', null, true); return TRUE; } catch (\Throwable $th) { $this->return_json(-2000,"","更新程序下载失败!"); } finally{ //再次判断更新程序是否存在 if( is_file("update.php") ) { //判断是否大约0 $file_size = filesize("update.php"); if( $file_size < 100 ) { $this->return_json(-2000,"","更新程序异常,请检查目录权限!"); } else{ return TRUE; } } else{ $this->return_json(-2000,"","更新程序下载失败,请检查目录权限!"); } } } /**更新升级程序 */ public function up_updater() { //如果不存在,则下载更新程序 if( !is_file("update.php") ) { if ( $this->down_updater() ) { $this->return_json(200,"","更新程序准备就绪!"); } } //如果存在更新程序,验证大小,大小不匹配时进行更新 if( is_file("update.tar.gz") ) { //获取header头 $header = get_headers(API_URL."/update.tar.gz",1); $lentgh = $header['Content-Length']; //获取文件大小 $file_size = filesize("update.tar.gz"); //如果本地文件大小和远程文件大小不一致,则下载更新 if ( $file_size != $lentgh ) { if ( $this->down_updater() ) { //更新完毕后提示 $this->return_json(200,"","更新程序更新完毕!"); } else{ $this->return_json(-2000,"","更新程序下载失败,请检查目录权限!"); } } else { $this->return_json(200,"","更新程序(压缩包)准备就绪!"); } } else if( is_file("update.php") ) { $this->return_json(200,"","更新程序(PHP)准备就绪!"); } else{ $this->return_json(200,"","更新程序(其它)准备就绪!"); } } /** * 校验更新程序 */ public function check_version($version) { //获取当前版本信息 $current_version = explode("-",file_get_contents("version.txt")); $current_version = str_replace("v","",$current_version[0]); //获取用户传递的版本 //$version = $_REQUEST['version']; if( $version == $current_version ) { $this->return_json(200,"","success"); } else{ $this->return_json(-2000,"","更新失败,版本校验不匹配,请检查目录权限!"); } } //curl get请求 protected function curl_get($url,$timeout = 10) { $curl = curl_init($url); #设置useragent curl_setopt($curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36"); curl_setopt($curl, CURLOPT_FAILONERROR, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); #设置超时时间,最小为1s(可选) curl_setopt($curl , CURLOPT_TIMEOUT, $timeout); $html = curl_exec($curl); curl_close($curl); return $html; } /** * name:数据库备份接口 */ public function backup_db(){ //验证请求 $this->auth($token); //验证订阅 $this->check_is_subscribe(); $backup_dir = 'data/backup/'; //判断目录是否存在,不存在则创建 if( !is_dir($backup_dir) ) { try { mkdir($backup_dir,0755); } catch (\Throwable $th) { $this->return_json(-2000,'','备份目录创建失败,请检查目录权限!'); } } //尝试拷贝数据库进行备份 try { //获取当前版本信息 $current_version = explode("-",file_get_contents("version.txt")); $current_version = str_replace("v","",$current_version[0]); $db_name = 'onenav_'.date("YmdHi",time()).'_'.$current_version.'.db3'; $backup_db_path = $backup_dir.$db_name; copy('data/onenav.db3',$backup_db_path); $this->return_json(200,$db_name,'success'); } catch (\Throwable $th) { $this->return_json(-2000,'','备份目录创建失败,请检查目录权限!'); } } /** * name:数据库备份列表 */ public function backup_db_list() { //验证请求 $this->auth($token); //验证订阅 $this->check_is_subscribe(); //备份目录 $backup_dir = 'data/backup/'; //遍历备份列表 $dbs = scandir($backup_dir); $newdbs = $dbs; //去除.和.. for ($i=0; $i < count($dbs); $i++) { if( ($dbs[$i] == '.') || ($dbs[$i] == '..') ) { unset($newdbs[$i]); } } //将删除后的数组重新赋值 $dbs = $newdbs; //获取备份列表个数 $num = count($dbs); //排序处理,按时间从大到小排序 rsort($dbs,2); //如果大于10个,则删减为10个 if( $num > 10 ) { for ($i=$num; $i > 10; $i--) { //物理删除数据库 unlink($backup_dir.$dbs[$i-1]); //删除数组最后一个元素 array_pop($dbs); } $count = 10; } else{ $count = $num; } //声明一个空数组 $data = []; //遍历数据库,获取时间,大小 foreach ($dbs as $key => $value) { $arr['id'] = $key; $arr['name'] = $value; $arr['mtime'] = date("Y-m-d H:i:s",filemtime($backup_dir.$value)); $arr['size'] = (filesize($backup_dir.$value) / 1024).'KB'; $data[$key] = $arr; } $datas = [ 'code' => 0, 'msg' => '', 'count' => $count, 'data' => $data ]; exit(json_encode($datas)); } /** * name:删除单个数据库备份 * @param $name:数据库名称 */ public function del_backup_db($name) { //验证请求 $this->auth($token); //验证订阅 $this->check_is_subscribe(); //使用正则表达式判断数据库名称是否合法 $pattern = '/^onenav_[0-9\-]+_[0-9.]+(db3)$/'; if( !preg_match_all($pattern,$name) ) { $this->return_json(-2000,'','数据库名称不合法!'); } //数据库目录 $backup_dir = 'data/backup/'; //删除数据库 try { unlink($backup_dir.$name); $this->return_json(200,'',"备份数据库已被删除!"); } catch (\Throwable $th) { $this->return_json(-2000,'',"删除失败,请检查目录权限!"); } } /** * name:恢复数据库备份 * @param $name:备份数据库名称 */ public function restore_db($name) { //验证请求 $this->auth($token); //验证订阅 $this->check_is_subscribe(); //使用正则表达式判断数据库名称是否合法 $pattern = '/^onenav_[0-9\-]+_[0-9.]+(db3)$/'; if( !preg_match_all($pattern,$name) ) { $this->return_json(-2000,'','数据库名称不合法!'); } //数据库目录 $backup_dir = 'data/backup/'; //恢复数据库 try { copy($backup_dir.$name,'data/onenav.db3'); $this->return_json(200,'','数据库已回滚为'.$name); } catch (\Throwable $th) { $this->return_json(-2000,'',"回滚失败,请检查目录权限!"); } } /** * name:获取OneNav信息 */ public function app_info($token) { //验证请求 $this->auth($token); //获取PHP版本 $data['php_version'] = PHP_VERSION; //获取OneNav版本 $data['onenav_version'] = file_get_contents("version.txt"); //获取分类数量 $data['cat_num'] = $this->db->count("on_categorys"); //获取链接数量 $data['link_num'] = $this->db->count("on_links"); //获取用户名 $data['username'] = USER; //返回JSON数据 $this->return_json(200,$data,"success"); } /** * name:下载数据库 */ public function down_db($name) { //验证请求 $this->auth($token); //使用正则表达式判断数据库名称是否合法 $pattern = '/^onenav_[0-9\-]+_[0-9.]+(db3)$/'; if( !preg_match_all($pattern,$name) ) { $this->return_json(-2000,'','数据库名称不合法!'); } //数据库目录 $backup_dir = 'data/backup/'; //拼接数据库路径 $full_path = $backup_dir.$name; if( !file_exists($full_path) ) { header('HTTP/1.1 404 NOT FOUND'); } else{ // 以只读和二进制模式打开文件 $file = fopen($full_path, "rb"); // 告诉浏览器这是一个文件流格式的文件 Header("Content-type: application/octet-stream"); // 请求范围的度量单位 Header("Accept-Ranges: bytes"); // Content-Length是指定包含于请求或响应中数据的字节长度 Header("Accept-Length: " . filesize($full_path)); // 用来告诉浏览器,文件是可以当做附件被下载,下载后的文件名称为$file_name该变量的值。 Header("Content-Disposition: attachment; filename=" . $name); // 读取文件内容并直接输出到浏览器 echo fread($file, filesize($full_path)); fclose($file); exit(); } } /** * name:创建分享 */ public function create_share($data) { //验证请求 $this->auth($token); //如果订阅不存在 if ( $this->is_subscribe() === FALSE ) { $this->return_json(-2000,'','此功能需要订阅后才能使用!'); } //设置默认数据 //随机8位分享ID $data['sid'] = GetRandStr(8); /** * 判断到期时间 */ //获取当前时间 $c_time = strtotime( $data['add_time'] ); $e_time = strtotime( $data['expire_time'] ); if( $c_time > $e_time ) { $this->return_json(-2000,'','到期日期不能小于当前日期!'); } /** * 判断密码 */ if( strlen($data['password']) > 16 ) { $this->return_json(-2000,'','密码长度不能超过16位!'); } $pattern = "/[A-Za-z0-9]{4,16}/"; //var_dump(preg_match($pattern,$data['password'])); if( !empty($data['password']) && !preg_match($pattern,$data['password']) ) { $this->return_json(-2000,'','密码只能由4-16位字母和数字组成!'); } //插入数据库 $result = $this->db->insert("on_shares",$data); if( $result ) { $this->return_json(200,'','success'); } else{ $this->return_json(-2000,'','写入数据库失败!'); } } /** * 分享列表 */ public function share_list($data){ //验证请求 $this->auth($token); $limit = $data['limit']; $offset = ($data['page'] - 1) * $data['limit']; //$fid = @$data['category_id']; $count = $this->db->count('on_shares','*'); $sql = "SELECT *,(SELECT name FROM on_categorys WHERE id = on_shares.cid) AS category_name FROM on_shares ORDER BY id DESC LIMIT {$limit} OFFSET {$offset}"; //原生查询 $datas = $this->db->query($sql)->fetchAll(); $datas = [ 'code' => 0, 'msg' => '', 'count' => $count, 'data' => $datas ]; exit(json_encode($datas)); } /** * name:根据分享的SID查询指定分类下的所有链接 */ public function get_sid_links($data) { //获得SID $sid = $data['sid']; //获得密码 $password = $data['password']; //判断SID是否合法 $pattern = "/[A-Za-z0-9]{8}$/"; if( (strlen($sid) !== 8) || !preg_match($pattern,$sid) ) { $this->return_json(-2000,'','SID不合法!'); } //根据SID查询得到分类ID $share_data = $this->db->get("on_shares","*",[ "sid" => $sid ]); //如果没有查询到数据 if( empty($share_data) ) { $this->return_json(-2000,'','SID不存在!'); } $cid = $share_data['cid']; //查询分类名称 $category_info = $this->db->get("on_categorys",["name"],[ "id" => $cid ]); $category_name = $category_info["name"]; //如果链接已经过期 $c_time = strtotime( date("Y-m-d H:i:s",time()) ); if ( $c_time > strtotime($share_data['expire_time']) ) { $this->return_json(-2000,'','链接已过期!'); } //如果分享密码不为空,则验证密码 if ( !empty($share_data['password']) && ( $share_data['password'] == $password) ) { //根据分类ID(cid)查询该分类下的所有链接 $results = $this->db->select("on_links","*",[ "fid" => $cid, "ORDER" => ["weight" => "DESC","id" => "DESC"] ]); $data = [ "category_name" => $category_name, "expire_time" => $share_data["expire_time"], "results" => $results ]; $this->return_json(200,$data,'success'); } else if ( empty($share_data['password']) ) { //根据分类ID(cid)查询该分类下的所有链接 $results = $this->db->select("on_links","*",[ "fid" => $cid, "ORDER" => ["weight" => "DESC","id" => "DESC"] ]); $data = [ "category_name" => $category_name, "expire_time" => $share_data["expire_time"], "results" => $results ]; $this->return_json(200,$data,'success'); } else{ $this->return_json(401,'','密码错误!'); } } /** * name:删除分享 */ public function del_share($data) { //验证请求 $this->auth($token); $id = $data['id']; //如果id为空 if( empty($id) ){ $this->return_json(-2000,$results,'ID不能为空!'); } $data = $this->db->delete('on_shares',[ 'id' => $id] ); if( $data ) { $this->return_json(200,'','success'); } else{ $this->return_json(-2000,'','删除失败!'); } } /** * name:获取站点信息,不需要授权 */ public function site_info() { //获取当前站点信息 $site = $this->db->get('on_options','value',[ 'key' => "s_site" ]); $site = unserialize($site); $this->return_json(200,$site,'success'); } /** * name:删除链接图标 */ public function del_link_icon(){ //验证授权 $this->auth($token); //获取图标路径 $icon_path = trim($_POST['icon_path']); //正则判断路径是否合法 $pattern = "/^data\/upload\/[0-9]+\/[0-9a-zA-Z]+\.(jpg|jpeg|png|bmp|gif|svg)$/"; //如果名称不合法,则终止执行 if( !preg_match($pattern,$icon_path) ){ $this->return_json(-2000,'','非法路径!'); } //继续执行 //检查图标是否存在 if( !is_file($icon_path) ) { $this->return_json(-2000,'','图标文件不存在,无需删除!'); } //执行删除操作 if( unlink($icon_path) ) { $this->return_json(200,'','success'); } else{ $this->return_json(-2000,'','图标删除失败,请检查目录权限!'); } } /** * name:优先使用POST获取数据,其次GET获取数据 */ protected function getData($param) { if(isset($_POST[$param])) { return $_POST[$param]; } elseif(isset($_GET[$param])) { return $_GET[$param]; } else { return null; } } /** * name: 全局搜索 */ public function global_search() { //验证授权 $this->auth($token); // 获取关键词 $keyword = htmlspecialchars( $this->getData("keyword") ); // 判断关键词长度 if( strlen($keyword) < 2 ) { $this->return_json(-2000,'','The length of the keyword is too short.'); } else if( strlen($keyword) > 32 ) { $this->return_json(-2000,'','The keyword length is too long'); } $keyword = '%'.$keyword.'%'; // 通过标题、链接、备用链接、描述进行模糊匹配 $data = $this->db->select('on_links', '*', [ "OR" => [ "title[~]" => $keyword, "url[~]" => $keyword, "url_standby[~]" => $keyword, "description[~]" => $keyword, ], "ORDER" => [ "weight" => "DESC" ] ]); // 查询出分类名称 $categorys = $this->db->select("on_categorys",[ 'id', 'name' ]); // 遍历分类,以id作为键名 foreach ($categorys as $category) { $newCategorys[$category['id']] = $category['name']; } // 遍历查询的数据,然后添加父级分类名称 foreach ($data as $key => $value) { $data[$key]['category_name'] = $newCategorys[$value['fid']]; } // 返回数据 $datas = [ 'code' => 0, 'msg' => '', 'count' => count($data), 'data' => $data ]; exit( json_encode($datas) ); } /** * 批量更新链接排序 * */ public function update_link_order(){ //验证授权 $this->auth($token); // 获取分类ID $cid = intval($this->getData("category_id")); // 获取json对象数据 // 获取前端传递的原始 JSON 数据 $rawData = file_get_contents('php://input'); // 将 JSON 数据转换为 PHP 关联数组 $jsonData = json_decode($rawData, true); // 检查解析是否成功 if (json_last_error() !== JSON_ERROR_NONE) { $this->return_json(-2000,'','JSON解析失败!'); } // 批量更新on_links表的weight字段 foreach ($jsonData as $key => $value) { $this->db->update('on_links', [ 'weight' => $value['weight'] ], [ 'fid' => $cid, 'id' => intval($value['id']) ]); } // 返回成功 $this->return_json(200,'','success'); } /** * name:获取订阅信息 */ public function get_subscribe() { //验证授权 $this->auth($token); //获取当前站点信息 $subscribe = $this->db->get('on_options','value',[ 'key' => "s_subscribe" ]); $subscribe = unserialize($subscribe); $this->return_json(200,$subscribe,'success'); } /** * 验证订阅是否有效 */ public function get_subscribe_status(){ // var_dump($_SERVER['HTTP_HOST']); //验证授权 $this->auth($token); // 获取订阅结果 $result = $this->is_subscribe(); if( $result === TRUE ) { $this->return_json(200,'Active','success'); } else{ $this->return_json(-2000,'','failure'); } } /** * name: 批量检测链接 * description:check_status,0:未检测,1:正常,2:异常,3:未知(比如启用了cf) */ public function batch_check_links(){ set_time_limit(1200); // 设置执行最大时间为20分钟 // 验证授权 $this->auth($token); // 验证订阅 $this->check_is_subscribe(); // 记录开始时间 $start_time = microtime(true); // 获取所有链接 $links = $this->db->select('on_links', '*'); // 设置并发限制(最大30个并发) $max_concurrent_requests = 30; $multi_curl = curl_multi_init(); // 初始化curl_multi $curl_handles = []; // 存储curl句柄 $link_count = count($links); // 获取链接数量 $completed_links = 0; // 已完成检测的链接数 // 设置curl超时时间为20分钟(1200秒) $timeout = 10; // 错误链接 $error_num = 0; // 并发处理每个链接 foreach ($links as $link) { // 创建一个curl句柄 $ch = curl_init(); // 设置curl选项 curl_setopt($ch, CURLOPT_URL, $link['url']); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 返回内容而不是输出 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); // 设置请求超时时间 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 设置连接超时时间 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); // 跟随重定向 // 设置UA curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36"); curl_setopt($ch, CURLOPT_HTTPGET, true); // 确保使用GET请求 curl_setopt($ch, CURLOPT_HEADER, true); // 获取响应头 // 将curl句柄加入到multi句柄中 curl_multi_add_handle($multi_curl, $ch); // 存储每个链接的ID,方便回调时更新状态 $curl_handles[$link['id']] = $ch; } // 执行curl请求并监听 do { $status = curl_multi_exec($multi_curl, $active); // 执行请求 if ($status > 0) { // 如果发生错误,输出错误信息 // echo "Curl error: " . curl_multi_strerror($status); } // 等待活动的请求完成 if ($active) { curl_multi_select($multi_curl, 1); // 阻塞直到有活动的请求 } } while ($active); // 处理每个请求的返回结果 foreach ($curl_handles as $id => $ch) { $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); // 获取HTTP状态码 $error = curl_error($ch); // 获取cURL错误信息 $header = curl_multi_getcontent($ch); // 获取完整的响应内容,包括头部 // 获取Server头 preg_match('/^Server:\s*(.*)$/mi', $header, $matches); $server_header = isset($matches[1]) ? $matches[1] : ''; // 获取Server头的值 // 获取当前时间戳 $last_checked_time = date('Y-m-d H:i:s'); // 定义 Web 服务器列表 $web_servers = [ 'cloudflare', 'waf', // 可以根据需要添加更多的 Web 服务器类型 'AkamaiGHost', 'JDCloudStarshield' ]; // 假设 $error 和 $http_code 已经定义 // 假设 $server_header 已经从响应头中获取 if ($error || $http_code >= 400) { // 默认状态为异常 $check_status = 2; // 异常 // 遍历 Web 服务器列表,检查是否包含任何已知的 Web 服务器标识 foreach ($web_servers as $server) { if (stripos($server_header, $server) !== false) { // 如果找到匹配的服务器,设置为未知状态 $check_status = 3; // 未知 break; // 一旦找到匹配的服务器,跳出循环 } } // 如果没有匹配到任何已知的 Web 服务器,则认为是异常 if ($check_status == 2) { // 错误数量+1 $error_num++; } } else if( $http_code === 0 ) { // HTTP 状态码为 0,表示链接超时,或者SSL证书过期 $check_status = 2; // 异常 // 错误数量+1 $error_num++; } else { // HTTP 状态码小于 400,表示正常 $check_status = 1; // 正常 } // 更新数据库字段 $this->db->update('on_links', [ 'check_status' => $check_status, 'last_checked_time' => $last_checked_time ], ['id' => $id]); // 删除curl句柄,释放资源 curl_multi_remove_handle($multi_curl, $ch); curl_close($ch); // 关闭curl句柄 // 完成一个链接的检测 $completed_links++; } // 关闭multi句柄 curl_multi_close($multi_curl); // 记录结束时间 $end_time = microtime(true); // 计算总共花费的时间 $elapsed_time = $end_time - $start_time; // 精确到s就行 $elapsed_time = round($elapsed_time, 2); // 返回成功 $this->return_json(200, [ 'completed_links' => $completed_links, 'elapsed_time' => $elapsed_time, 'error_num' => $error_num ], 'success'); } // 获取过渡页API public function transition_page(){ //获取当前站点信息 $transition_page = $this->db->get('on_options','value',[ 'key' => "s_transition_page" ]); $transition_page = unserialize($transition_page); // 返回数据 $this->return_json(200,$transition_page,'success'); } /** * AI检索 */ public function ai_search() { set_time_limit(1200); // 设置执行最大时间为20分钟 // 验证授权 $this->auth($token); // 验证订阅 $this->check_is_subscribe(); // 获取用户输入 $content = $_GET['content']; // 查询出所有链接,只需要url, title, description, url_standby字段 $links = $this->db->select('on_links', ['url', 'title', 'description', 'url_standby']); // 将链接数据转换为AI需要的JSON格式 $bookmarks = []; foreach ($links as $link) { $bookmarks[] = [ 'title' => $link['title'], 'url' => $link['url'], 'url_standby' => $link['url_standby'], 'description' => $link['description'] ]; } // 将数据转换为JSON格式 $bookmarks = json_encode($bookmarks); // 创建AI请求的消息内容 $messages = [ [ "role" => "system", "content" => "我会给你一段JSON格式的书签数据,其中包含每个链接的标题、URL、标签和描述等信息。你需要根据用户提供的指令或关键词,结合你所学的知识判断,并智能匹配与之相关的链接。请根据关键词的相关性来排序匹配结果,并返回匹配的链接列表以及对应的名称。 在返回匹配的链接时,确保: 1. 返回的链接与用户提供的关键词高度相关,优先匹配精确相关的内容。 2. 根据相关性将结果按从高到低排序,以确保最相关的链接出现在列表的前面。 3. 对于匹配结果,再根据你所学的知识推荐额外的5个相关链接,确保这些推荐的链接也与用户的需求相关。 例如,用户输入“AI技术”,如果书签数据中有相关的AI资源,你需要返回相关的链接列表,并根据关键词“AI”排序结果。同时,你还应该推荐额外的5个相关链接,帮助用户发现更多有价值的资源。 " ], [ "role" => "user", "content" => $bookmarks // 你可以根据实际需求修改用户输入 ], [ "role" => "user", "content" => $content // 你可以根据实际需求修改用户输入 ] ]; // var_dump($messages); // 发送请求到AI接口 $response = $this->send_to_ai($bookmarks, $messages); echo $response; } private function send_to_ai($bookmarks, $messages) { // 准备请求数据 $data = [ 'model' => 'qwen-plus', 'messages' => $messages, 'stream' => false ]; // 设置请求头和授权信息 $headers = [ 'Content-Type: application/json', 'Authorization: Bearer sk-xxx' // 用你的实际API密钥替换 ]; // 使用cURL发送请求 $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); // 获取响应并关闭cURL $response = curl_exec($ch); curl_close($ch); // var_dump($response); // exit; // 解析响应 return $response; } }