码桶

发现社区成员的开源项目

开心 /

kxpyq

公开
main
kxpyq/ajax.php
ajax.php13.2 KB
<?php
/**
 * KXpyq Theme AJAX Handler
 * 
 * 独立的AJAX处理文件,不依赖Typecho的插件钩子系统
 */

// 加载Typecho核心
if (!defined('__TYPECHO_ROOT_DIR__')) {
    define('__TYPECHO_ROOT_DIR__', dirname(dirname(dirname(dirname(__FILE__)))));
}
if (!@include_once __TYPECHO_ROOT_DIR__ . '/config.inc.php') {
    http_response_code(500);
    echo json_encode(['code' => 1, 'msg' => 'Config not found']);
    exit;
}
\Typecho\Common::init();

// 初始化插件系统(加载所有已激活插件的钩子)
$pluginsActivated = $db->fetchRow($db->select('value')->from('table.options')->where('name = ?', 'plugins'));
if ($pluginsActivated) {
    \Typecho\Plugin::init(json_decode($pluginsActivated['value'], true));
}

// 加载主题函数
require_once __TYPECHO_ROOT_DIR__ . '/usr/themes/kxpyq/functions.php';

$db = \Typecho\Db::get();
$request = \Typecho\Request::getInstance();
$action = $request->get('act', '');

header('Content-Type: application/json; charset=utf-8');

switch ($action) {
    case 'load':
        pyq_ajax_load($request, $db);
        break;
    case 'like':
        pyq_ajax_like($request, $db);
        break;
    case 'comment':
        pyq_ajax_comment($request, $db);
        break;
    default:
        echo json_encode(['code' => 1, 'msg' => 'Unknown action']); break;
}

exit;

/**
 * 加载更多说说
 */
function pyq_ajax_load($request, $db) {
    $page = max(1, intval($request->get('page', 1)));
    $limit = min(20, max(1, intval($request->get('limit', 10))));
    $offset = ($page - 1) * $limit;
    
    $options = \Widget\Options::alloc();
    $category_slug = $options->category_slug ?: 'shuoshuo';
    
    $category = $db->fetchRow($db->select()->from('table.metas')
        ->where('table.metas.type = ?', 'category')
        ->where('table.metas.slug = ?', $category_slug));
    
    if (!$category) {
        echo json_encode(['code' => 0, 'data' => [], 'hasMore' => false]);
        return;
    }
    
    $cid_list = $db->fetchAll($db->select('table.relationships.cid')
        ->from('table.relationships')
        ->where('table.relationships.mid = ?', $category['mid']));
    
    $cids = array_column($cid_list, 'cid');
    
    if (empty($cids)) {
        echo json_encode(['code' => 0, 'data' => [], 'hasMore' => false]);
        return;
    }
    
    $posts = $db->fetchAll($db->select()->from('table.contents')
        ->where('table.contents.type = ?', 'post')
        ->where('table.contents.status = ?', 'publish')
        ->where('table.contents.parent = 0')
        ->where('table.contents.cid IN ?', $cids)
        ->order('table.contents.created', \Typecho\Db::SORT_DESC)
        ->limit($limit)
        ->offset($offset));
    
    // 置顶排序:置顶帖子排最前面
    $pinned_cids = [];
    $top_rows = $db->fetchAll($db->select('table.fields.cid')->from('table.fields')
        ->where('table.fields.name = ?', 'is_top')
        ->where('table.fields.str_value = ?', '1')
        ->where('table.fields.cid IN ?', $cids));
    foreach ($top_rows as $r) { $pinned_cids[] = $r['cid']; }
    
    $pinned = []; $normal = [];
    foreach ($posts as $post) {
        if (in_array($post['cid'], $pinned_cids)) $pinned[] = $post;
        else $normal[] = $post;
    }
    $posts = array_merge($pinned, $normal);
    
    $data = [];
    foreach ($posts as $post) {
        $data[] = pyq_format_post_data($post, $db, $options);
    }
    
    $total = $db->fetchRow($db->select(array('COUNT(1)' => 'total'))
        ->from('table.contents')
        ->where('table.contents.type = ?', 'post')
        ->where('table.contents.status = ?', 'publish')
        ->where('table.contents.parent = 0')
        ->where('table.contents.cid IN ?', $cids));
    
    $hasMore = ($offset + $limit) < $total['total'];
    
    echo json_encode(['code' => 0, 'data' => $data, 'hasMore' => $hasMore]);
}

/**
 * 点赞
 */
function pyq_ajax_like($request, $db) {
    $cid = intval($request->get('cid', 0));
    if ($cid <= 0) {
        echo json_encode(['code' => 1, 'msg' => 'Invalid cid']);
        return;
    }
    
    $ip = pyq_get_ip();
    $cookie_key = 'pyq_like_' . $cid;
    $is_liked = isset($_COOKIE[$cookie_key]);
    
    $like_field = $db->fetchRow($db->select()->from('table.fields')
        ->where('table.fields.cid = ?', $cid)
        ->where('table.fields.name = ?', 'likes'));
    
    $likes = [];
    if ($like_field && !empty($like_field['str_value'])) {
        $likes = json_decode($like_field['str_value'], true) ?: [];
    }
    
    if ($is_liked) {
        $likes = array_filter($likes, function($l) use ($ip) {
            return !isset($l['ip']) || $l['ip'] !== $ip;
        });
        $likes = array_values($likes);
        setcookie($cookie_key, '', time() - 3600, '/');
    } else {
        $like_name = !empty($_COOKIE['__typecho_remember_author']) ? $_COOKIE['__typecho_remember_author'] : '访客';
        $likes[] = ['ip' => $ip, 'name' => $like_name, 'time' => time()];
        setcookie($cookie_key, '1', time() + 86400 * 365, '/');
    }
    
    $likes_json = json_encode($likes);
    
    if ($like_field) {
        $db->query($db->update('table.fields')
            ->rows(['str_value' => $likes_json])
            ->where('table.fields.cid = ?', $cid)
            ->where('table.fields.name = ?', 'likes'));
    } else {
        $db->query($db->insert('table.fields')
            ->rows([
                'cid' => $cid, 'name' => 'likes', 'type' => 'str',
                'str_value' => $likes_json, 'int_value' => 0, 'float_value' => 0,
            ]));
    }
    
    echo json_encode([
        'code' => 0, 'liked' => !$is_liked, 'count' => count($likes), 'likes' => $likes,
    ]);
}

/**
 * 获取IP归属地
 * 优先使用本地 ip2region.xdb(如存在),否则回退 ip-api.com
 */
function pyq_get_ip_location($ip) {
    if (empty($ip) || $ip === '127.0.0.1' || $ip === '::1') return '';
    if (strpos($ip, '192.168.') === 0 || strpos($ip, '10.') === 0) return '';
    
    // 尝试本地 ip2region
    $xdb = __DIR__ . '/lib/ip2region/ip2region.xdb';
    if (file_exists($xdb) && filesize($xdb) > 1024) {
        $result = pyq_ip2region_search($ip, $xdb);
        if ($result) return $result;
    }
    
    // 回退 ip-api.com
    $url = "http://ip-api.com/json/{$ip}?lang=zh-CN&fields=regionName,city";
    $ctx = stream_context_create(['http'=>['timeout'=>3]]);
    $data = @file_get_contents($url, false, $ctx);
    if ($data) {
        $json = json_decode($data, true);
        if ($json && isset($json['regionName'])) {
            $loc = $json['regionName'];
            if (!empty($json['city']) && $json['city'] !== $json['regionName']) {
                $loc .= ' · ' . $json['city'];
            }
            return $loc;
        }
    }
    return '';
}

/**
 * ip2region 本地查询
 */
function pyq_ip2region_search($ip, $xdb) {
    try {
        require_once __DIR__ . '/lib/ip2region/xdb/Searcher.class.php';
        $searcher = \ip2region\xdb\Searcher::newWithFileOnly(\ip2region\xdb\IPv4::default(), $xdb);
        $region = $searcher->search($ip);
        if ($region) {
            // ip2region 返回格式: 国家|区域|省份|城市|ISP
            $parts = explode('|', $region);
            $province = trim($parts[2] ?? '');
            $city = trim($parts[3] ?? '');
            if ($province === '0' || $province === '') return '';
            // 去掉省/市/自治区后缀使显示更简洁
            $province = preg_replace('/(省|市|自治区|特别行政区|维吾尔|回族|壮族|藏族|土家族|苗族|彝族|蒙古族|朝鲜族)$/u', '', $province);
            if ($city && $city !== '0' && $city !== $province) {
                return $province . ' · ' . preg_replace('/市$/u', '', $city);
            }
            return $province;
        }
    } catch (\Exception $e) {}
    return '';
}

/**
 * 评论
 */
function pyq_ajax_comment($request, $db) {
    $cid = intval($request->get('cid', 0));
    $author = trim($request->get('author', ''));
    $mail = trim($request->get('mail', ''));
    $url = trim($request->get('url', ''));
    $text = trim($request->get('text', ''));
    $parent = intval($request->get('parent', 0));
    
    if ($cid <= 0 || empty($text)) {
        echo json_encode(['code' => 1, 'msg' => '缺少必要参数']);
        return;
    }
    
    if (empty($author)) $author = '匿名访客';
    
    if (empty($mail) || !filter_var($mail, FILTER_VALIDATE_EMAIL)) {
        echo json_encode(['code' => 1, 'msg' => '请填写有效邮箱']);
        return;
    }
    
    // 获取IP归属地
    $ip = pyq_get_ip();
    $ip_location = pyq_get_ip_location($ip);
    
    $data = [
        'cid' => $cid, 'created' => time(),
        'author' => htmlspecialchars($author), 'authorId' => 0, 'ownerId' => 0,
        'mail' => htmlspecialchars($mail), 'url' => htmlspecialchars($url),
        'ip' => $ip, 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
        'text' => htmlspecialchars($text), 'type' => 'comment', 'status' => 'approved',
        'parent' => $parent, 'location' => $ip_location,
    ];
    
    $result = $db->query($db->insert('table.comments')->rows($data));
    $coid = $result;
    
    $post = $db->fetchRow($db->select('commentsNum')->from('table.contents')
        ->where('table.contents.cid = ?', $cid));
    if ($post) {
        $db->query($db->update('table.contents')
            ->rows(['commentsNum' => $post['commentsNum'] + 1])
            ->where('table.contents.cid = ?', $cid));
    }
    
    // 检查是否为博主
    $is_owner = false;
    $owner = $db->fetchRow($db->select('mail')->from('table.users')->where('table.users.group=?', 'administrator'));
    if ($owner && $mail === $owner['mail']) {
        $is_owner = true;
    }
    
    // 通过Typecho插件钩子触发邮件通知(CommentNotifier等插件自动响应)
    $email_sent = false;
    try {
        // 加载刚插入的评论
        $feedback = Typecho_Widget::widget('Widget_Base_Comments');
        $feedback->push($db->fetchRow($db->select()->from('table.comments')->where('coid = ?', $coid)));
        // 通过CommentNotifier发送邮件通知
        $email_sent = false;
        try {
            // 查询文章作者uid,补全ownerId(comments表该字段为0)
            $post_row = $db->fetchRow($db->select('authorId')->from('table.contents')->where('cid = ?', $cid));
            if ($post_row) {
                $feedback->ownerId = intval($post_row['authorId']);
            }
            require_once __DIR__ . '/../../plugins/CommentNotifier/Plugin.php';
            \TypechoPlugin\CommentNotifier\Plugin::refinishComment($feedback);
            $email_sent = true;
        } catch (\Exception $e) {
            error_log('[KXpyq CommentNotifier] ' . $e->getMessage());
        }
    } catch (\Exception $e) {
        error_log('[KXpyq CommentNotifier] ' . $e->getMessage());
    }

    echo json_encode([
        'code' => 0,
        'data' => [
            'coid' => $coid, 'author' => htmlspecialchars($author),
            'text' => htmlspecialchars($text), 'parent' => $parent,
            'created' => time(), 'time' => '刚刚',
            'location' => $ip_location,
            'is_owner' => $is_owner,
            'email_sent' => $email_sent,
        ],
    ]);
}

function pyq_get_ip() {
    $keys = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR'];
    foreach ($keys as $key) {
        if (!empty($_SERVER[$key])) {
            $ip = explode(',', $_SERVER[$key])[0];
            if (filter_var(trim($ip), FILTER_VALIDATE_IP)) return trim($ip);
        }
    }
    return '0.0.0.0';
}

/**
 * 搜索
 */
function pyq_ajax_search($request, $db) {
    $q = trim($request->get('q', ''));
    if (empty($q) || mb_strlen($q) < 2) {
        echo json_encode([]);
        return;
    }
    
    $options = \Typecho\Widget::widget('Widget_Options');
    $slug = $options->category_slug ?: 'shuoshuo';
    
    // 获取说说分类的cid
    $cat = $db->fetchRow($db->select()->from('table.metas')
        ->where('table.metas.type=?','category')
        ->where('table.metas.slug=?',$slug));
    
    $cids = [];
    if ($cat) {
        $cid_rows = $db->fetchAll($db->select('table.relationships.cid')
            ->from('table.relationships')
            ->where('table.relationships.mid=?',$cat['mid']));
        $cids = array_column($cid_rows,'cid');
    }
    
    if (empty($cids)) {
        echo json_encode([]);
        return;
    }
    
    // 搜索
    $like_q = '%' . $q . '%';
    $posts = $db->fetchAll($db->select()
        ->from('table.contents')
        ->where('table.contents.type=?','post')
        ->where('table.contents.status=?','publish')
        ->where('table.contents.parent=0')
        ->where('table.contents.cid IN ?',$cids)
        ->where('table.contents.text LIKE ? OR table.contents.title LIKE ?', $like_q, $like_q)
        ->order('table.contents.created',\Typecho\Db::SORT_DESC)
        ->limit(20));
    
    $results = [];
    foreach ($posts as $post) {
        $text = strip_tags($post['text']);
        $excerpt = mb_substr($text, 0, 100) . (mb_strlen($text) > 100 ? '...' : '');
        $results[] = [
            'cid' => $post['cid'],
            'title' => $post['title'] ?: mb_substr($text, 0, 30),
            'excerpt' => $excerpt,
            'permalink' => $options->siteUrl . $post['cid'] . '.html',
        ];
    }
    
    echo json_encode($results);
}

/**
 * 阅读计数
 */