Initial commit

ZnoteX / Commit #5

Commit Initial commit

Alex Alex committed 01/10/2026 09:20 main Full upload
481 files +128,311 -0
A admin/modules/_partials/plugins_browse.php +186-0 View file
@@ -0,0 +1,186 @@
1+<?php
2+
3+if (!defined('ACP_ROOT')) {
4+ http_response_code(403);
5+ die('Direct access denied.');
6+}
7+
8+$repoConfig = plugin_repository_config();
9+$catalogue = plugin_repository_list(isset($_GET['refresh']));
10+$remote = $catalogue['plugins'];
11+$repoError = (string)($catalogue['error'] ?? '');
12+$installed = znote_plugins(true);
13+?>
14+
15+<div class="acp-toolbar">
16+ <div>
17+ <a class="acp-btn acp-btn--ghost acp-btn--sm" href="<?= h(acp_url('plugins')) ?>">
18+ <i class="fa fa-arrow-left"></i> <?= t('acp.plgbr.back') ?>
19+ </a>
20+ </div>
21+ <div class="acp-actions is-tight">
22+ <span class="is-muted"><?= t('acp.plgbr.available', ['n' => count($remote)]) ?></span>
23+ <a class="acp-btn acp-btn--ghost acp-btn--sm" href="<?= h(acp_url('plugins', ['tab' => 'browse', 'refresh' => 1])) ?>">
24+ <i class="fa fa-refresh"></i> <?= t('acp.plgbr.refresh') ?>
25+ </a>
26+ </div>
27+</div>
28+
29+<?php if (!$repoConfig['enabled']): ?>
30+
31+ <section class="acp-card">
32+ <div class="acp-card-body">
33+ <?php acp_empty(t('acp.plgbr.repo_off'), 'fa-plug'); ?>
34+ <p class="is-muted" style="text-align:center;">
35+ <?= t('acp.plgbr.repo_off_hint', ['code' => '<code>$config[\'plugin_repository\'][\'enabled\'] = true;</code>']) ?>
36+ </p>
37+ </div>
38+ </section>
39+
40+<?php elseif ($repoError !== ''): ?>
41+
42+ <div class="acp-actions is-tight" style="margin-bottom:12px;">
43+ <form class="acp-inline-form" method="post">
44+ <?= acp_csrf_field() ?>
45+ <input type="hidden" name="clear_plugin_repository_cache" value="1">
46+ <button class="acp-btn acp-btn--red acp-btn--sm" type="submit">
47+ <i class="fa fa-trash"></i> <?= t('acp.plgbr.delete_cache') ?>
48+ </button>
49+ </form>
50+ </div>
51+
52+ <div class="acp-flash acp-flash--error">
53+ <i class="fa fa-exclamation-triangle"></i>
54+ <span>
55+ <strong><?= t('acp.plgbr.catalogue_error') ?></strong><br>
56+ <?= h($repoError) ?><br>
57+ <span class="is-muted"><?= t('acp.plgbr.catalogue_url') ?> <code><?= h($repoConfig['index']) ?></code></span>
58+ </span>
59+ </div>
60+
61+<?php elseif (!$remote): ?>
62+
63+ <section class="acp-card">
64+ <div class="acp-card-body">
65+ <?php acp_empty(t('acp.plgbr.catalogue_empty'), 'fa-inbox'); ?>
66+ </div>
67+ </section>
68+
69+<?php else: ?>
70+
71+ <div class="acp-flash acp-flash--info">
72+ <i class="fa fa-info-circle"></i>
73+ <span>
74+ <?= t('acp.plgbr.warning', [
75+ 'hosts' => '<code>' . h(implode('</code>, <code>', $repoConfig['allowed_hosts'])) . '</code>',
76+ 'configphp' => '<code>config.php</code>',
77+ ]) ?>
78+ </span>
79+ </div>
80+
81+ <div class="acp-media">
82+ <?php foreach ($remote as $key => $item):
83+ $isInstalled = isset($installed[$key]);
84+ $localVer = $isInstalled ? (string)$installed[$key]['version'] : '';
85+ $isUpdate = ($isInstalled && $item['version'] !== '' && $localVer !== '' && version_compare($item['version'], $localVer, '>'));
86+ ?>
87+ <article class="acp-media-item<?= $isInstalled && !$isUpdate ? ' is-dimmed' : '' ?>">
88+
89+ <?php if ($item['screenshot'] !== ''): ?>
90+ <img src="<?= h($item['screenshot']) ?>" alt="<?= h($item['name']) ?>" loading="lazy" referrerpolicy="no-referrer">
91+ <?php else: ?>
92+ <div style="display:grid;place-items:center;height:170px;background:var(--acp-panel-2);color:var(--acp-fg-muted);">
93+ <span><i class="fa fa-plug"></i> &nbsp;<?= t('acp.plgbr.no_screenshot') ?></span>
94+ </div>
95+ <?php endif; ?>
96+
97+ <div class="acp-media-body">
98+ <h3>
99+ <?= h($item['name']) ?>
100+ <?php if ($isUpdate): ?>
101+ <span class="acp-pill acp-pill--amber"><?= t('acp.plgbr.update_available') ?></span>
102+ <?php elseif ($isInstalled): ?>
103+ <span class="acp-pill acp-pill--grey"><?= t('acp.plgbr.installed_pill') ?></span>
104+ <?php endif; ?>
105+ </h3>
106+
107+ <p><?= h($item['description']) ?></p>
108+
109+ <?php if ($isUpdate && (string)($item['changelog'] ?? '') !== ''): ?>
110+ <div style="margin:10px 0;padding:10px 12px;border:1px solid var(--acp-border);border-left:3px solid var(--acp-amber);border-radius:8px;background:var(--acp-panel-2);font-size:12px;">
111+ <strong style="display:block;margin-bottom:5px;">
112+ <i class="fa fa-list-ul"></i>
113+ <?= t('acp.plgbr.whats_new', ['version' => h($item['version'])]) ?>
114+ </strong>
115+ <div class="is-muted" style="white-space:pre-line;line-height:1.45;"><?= h($item['changelog']) ?></div>
116+ </div>
117+ <?php endif; ?>
118+
119+ <p class="is-muted" style="font-size:12px;">
120+ <code>plugins/<?= h($key) ?>/</code>
121+ <?php if ($item['author'] !== ''): ?>&middot; <?= t('acp.plgbr.by_author', ['author' => h($item['author'])]) ?><?php endif; ?>
122+ <?php if ($item['version'] !== ''): ?>
123+ &middot; v<?= h($item['version']) ?>
124+ <?php if ($isUpdate): ?>
125+ <span class="acp-pill acp-pill--amber"><?= t('acp.plgbr.you_have', ['version' => h($localVer)]) ?></span>
126+ <?php endif; ?>
127+ <?php endif; ?>
128+ </p>
129+
130+ <?php if (!$item['installable']): ?>
131+ <p style="font-size:12px;color:var(--acp-red);"><?= t('acp.plgbr.not_installable') ?></p>
132+ <?php endif; ?>
133+ </div>
134+
135+ <div class="acp-media-foot">
136+ <?php if (!$item['installable']): ?>
137+
138+ <?php if ($item['url'] !== ''): ?>
139+ <a class="acp-btn acp-btn--ghost acp-btn--sm" href="<?= h($item['url']) ?>" target="_blank" rel="noopener">
140+ <i class="fa fa-external-link"></i> <?= t('acp.plgbr.open_page') ?>
141+ </a>
142+ <?php endif; ?>
143+
144+ <?php elseif ($isInstalled && !$isUpdate): ?>
145+
146+ <span class="acp-btn acp-btn--ghost acp-btn--sm is-disabled">
147+ <i class="fa fa-check"></i> <?= t('acp.plgbr.installed_badge') ?>
148+ </span>
149+ <form class="acp-inline-form" method="post"
150+ data-confirm="<?= h(t('acp.plgbr.confirm_reinstall', ['name' => $item['name'], 'key' => $key])) ?>">
151+ <?= acp_csrf_field() ?>
152+ <input type="hidden" name="repo_install" value="<?= h($key) ?>">
153+ <input type="hidden" name="overwrite" value="1">
154+ <button class="acp-btn acp-btn--ghost acp-btn--sm" type="submit">
155+ <i class="fa fa-refresh"></i> <?= t('acp.plgbr.reinstall') ?>
156+ </button>
157+ </form>
158+
159+ <?php else: ?>
160+
161+ <form class="acp-inline-form" method="post"
162+ data-confirm="<?= $isInstalled
163+ ? h(t('acp.plgbr.confirm_update', ['name' => $item['name'], 'version' => $item['version'], 'key' => $key]))
164+ : h(t('acp.plgbr.confirm_install', ['name' => $item['name']])) ?>">
165+ <?= acp_csrf_field() ?>
166+ <input type="hidden" name="repo_install" value="<?= h($key) ?>">
167+ <?php if ($isInstalled): ?><input type="hidden" name="overwrite" value="1"><?php endif; ?>
168+ <button class="acp-btn acp-btn--sm" type="submit">
169+ <i class="fa fa-<?= $isInstalled ? 'arrow-up' : 'download' ?>"></i>
170+ <?= $isInstalled ? t('acp.plgbr.update_btn') : t('acp.plgbr.install_btn') ?>
171+ </button>
172+ </form>
173+
174+ <?php endif; ?>
175+
176+ <?php if ($item['url'] !== '' && $item['installable']): ?>
177+ <a class="acp-btn acp-btn--ghost acp-btn--sm" href="<?= h($item['url']) ?>" target="_blank" rel="noopener">
178+ <i class="fa fa-external-link"></i> <?= t('acp.plgbr.details') ?>
179+ </a>
180+ <?php endif; ?>
181+ </div>
182+ </article>
183+ <?php endforeach; ?>
184+ </div>
185+
186+<?php endif; ?>
A admin/modules/_template.php +73-0 View file
@@ -0,0 +1,73 @@
1+<?php
2+/**
3+ * Title: My New Page
4+ * Icon: fa-cube
5+ * Group: Other
6+ * Order: 100
7+ * Description: One line shown under the page title.
8+ */
9+
10+/*
11+ * ---------------------------------------------------------------------------
12+ * How to add an admin page
13+ * ---------------------------------------------------------------------------
14+ *
15+ * 1. Copy this file to admin/modules/<name>.php (no leading underscore -
16+ * files starting with "_" are skipped by the registry).
17+ * 2. Edit the docblock above. That IS the menu entry: Title, Icon
18+ * (Font Awesome 4.7 class), Group and Order. Nothing else to register.
19+ * 3. Write your logic and markup below. Available to you already:
20+ *
21+ * $config, $user_data, $session_user_id, $version from engine/init.php
22+ * h(), intv() escaping helpers
23+ * acp_url(), acp_site(), acp_redirect() links and redirects
24+ * acp_csrf_field() CSRF token input
25+ * acp_flash_success/error/info() messages across redirects
26+ * acp_card_open/close(), acp_empty(), acp_stat() layout blocks
27+ * db()->fetchOne/fetchAll($sql, $params) prepared SELECT, use ? placeholders
28+ * db()->execute($sql, $params) prepared INSERT/UPDATE/DELETE
29+ * db()->transaction(function ($db) { ... }) several writes, all or nothing
30+ *
31+ * Notes:
32+ * - The working directory is the project root, so 'engine/cache/x' and
33+ * other engine-relative paths work exactly as they do on a normal page.
34+ * - Links to the public site need acp_site(): acp_site('index.php').
35+ * - Do NOT include init.php, the header or the footer - index.php does that.
36+ * - POST requests are CSRF-checked centrally, so every form needs
37+ * <?= acp_csrf_field() ?> or it will be rejected with a 400.
38+ * - Redirect after a successful POST with acp_redirect('<name>') so a
39+ * refresh does not resubmit.
40+ * - To give the menu entry a counter bubble, add a function named
41+ * acp_badge_<name>() to admin/bootstrap.php returning an int.
42+ */
43+
44+if (!defined('ACP_ROOT')) {
45+ http_response_code(403);
46+ die('Direct access denied.');
47+}
48+
49+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
50+ // ... do the work ...
51+ acp_flash_success('Saved.');
52+ acp_redirect('_template');
53+}
54+?>
55+
56+<section class="acp-card">
57+ <header class="acp-card-head">
58+ <h2>Example form</h2>
59+ <p>Delete everything below and write your own</p>
60+ </header>
61+ <div class="acp-card-body">
62+ <form method="post">
63+ <?= acp_csrf_field() ?>
64+ <div class="acp-field">
65+ <label class="acp-label" for="example">Something</label>
66+ <input class="acp-input" id="example" name="example" placeholder="Type here">
67+ </div>
68+ <div class="acp-actions">
69+ <button class="acp-btn" type="submit"><i class="fa fa-check"></i> Save</button>
70+ </div>
71+ </form>
72+ </div>
73+</section>
A api/api.php +88-0 View file
@@ -0,0 +1,88 @@
1+<?php
2+// PHP version check
3+if (PHP_VERSION_ID < 80100) {
4+ die('PHP 8.1 or higher is required.');
5+}
6+
7+if (!isset($filepath)) {
8+ $filepath = '../';
9+}
10+
11+$version = '2.0.1';
12+
13+ob_start();
14+require_once $filepath.'config.php';
15+require_once $filepath.'engine/session.php';
16+require_once $filepath.'engine/security.php';
17+znote_session_start((array)($config['session'] ?? array()));
18+znote_security_boot((array)($config['security'] ?? array()));
19+
20+$sessionPrefix = $config['session_prefix'];
21+
22+$config['ServerEngineReal'] = $config['ServerEngine'] ?? 'TFS_10';
23+if (in_array($config['ServerEngineReal'], array('TFS_16', 'CANARY', 'BLACKTEK'), true)) {
24+ $config['ServerEngine'] = 'TFS_10';
25+ $config['TFSVersion'] = 'TFS_10';
26+}
27+if ($config['ServerEngineReal'] === 'CANARY') {
28+ $config['twoFactorAuthenticator'] = false;
29+}
30+
31+require_once $filepath.'engine/database/connect.php';
32+require_once $filepath.'engine/function/general.php';
33+require_once $filepath.'engine/function/users.php';
34+require_once $filepath.'engine/function/cache.php';
35+require_once $filepath.'engine/adapter/ServerAdapterInterface.php';
36+require_once $filepath.'engine/adapter/TFSAdapter.php';
37+require_once $filepath.'engine/adapter/CanaryAdapter.php';
38+require_once $filepath.'engine/adapter/OtHireAdapter.php';
39+require_once $filepath.'engine/adapter/BlackTekAdapter.php';
40+require_once $filepath.'engine/adapter/factory.php';
41+
42+// Default API config
43+$config['api']['debug'] ??= false;
44+
45+$response = [
46+ 'version' => [
47+ 'znote' => $version,
48+ 'ot' => $config['ServerEngine'] ?? null
49+ ],
50+];
51+
52+if (isset($moduleVersion)) {
53+ $response['version']['module'] = $moduleVersion;
54+}
55+
56+function UseClass($name = false, $module = false, $path = false) {
57+ if ($name === false) {
58+ throw new InvalidArgumentException('UseClass(): class parameter is false.');
59+ }
60+
61+ $names = is_array($name) ? $name : [$name];
62+
63+ foreach ($names as $class) {
64+ $mod = $module ?: $class;
65+ $file = $path
66+ ? "{$path}/{$class}.php"
67+ : __DIR__ . "/modules/base/{$mod}/class/{$class}.php";
68+
69+ if (!file_exists($file)) {
70+ throw new RuntimeException("Class file not found: {$file}");
71+ }
72+
73+ require_once $file;
74+ }
75+}
76+
77+function SendResponse(array $response): void {
78+ global $config;
79+
80+ if ($config['api']['debug']) {
81+ data_dump($response, false, "Response (debug mode)");
82+ return;
83+ }
84+
85+ header('Content-Type: application/json; charset=utf-8');
86+ echo json_encode($response);
87+}
88+?>
A api/index.php +77-0 View file
@@ -0,0 +1,77 @@
1+<?php
2+$filepath = '../';
3+require_once 'module.php';
4+
5+// Autofetch API modules
6+$directory = 'modules';
7+$plugins = [];
8+
9+// Load base
10+$plugins['base'] = [
11+ 'player' => 'test.php'
12+];
13+
14+$baseIterator = new DirectoryIterator($directory);
15+
16+foreach ($baseIterator as $dir) {
17+ if ($dir->isDot() || !$dir->isDir()) {
18+ continue;
19+ }
20+
21+ $moduleName = $dir->getFilename();
22+ $moduleIterator = new DirectoryIterator($dir->getPathname());
23+
24+ foreach ($moduleIterator as $file) {
25+ if (!$file->isFile()) {
26+ continue;
27+ }
28+
29+ if ($file->getExtension() !== 'php') {
30+ continue;
31+ }
32+
33+ $plugins[$moduleName][] = $file->getFilename();
34+ }
35+}
36+
37+// Global data
38+$response['modules'] = $plugins;
39+$response['data']['title'] = $config['site_title'] ?? '';
40+$response['data']['slogan'] = $config['site_title_context'] ?? '';
41+$response['data']['time'] = getClock(time(), false, true);
42+$response['data']['time_formatted'] = getClock(time(), true, true);
43+
44+// Account count
45+$accounts = db()->fetchOne("SELECT COUNT(*) AS `count` FROM `accounts`");
46+$response['data']['accounts'] = (int)($accounts['count'] ?? 0);
47+
48+// Player count
49+$players = db()->fetchOne("SELECT COUNT(*) AS `count` FROM `players`");
50+$response['data']['players'] = (int)($players['count'] ?? 0);
51+
52+// Online players
53+if (znote_server_adapter()->normalizedEngine() !== 'TFS_10') {
54+ $online = db()->fetchOne("
55+ SELECT COUNT(*) AS `count`, COUNT(DISTINCT `lastip`) AS `unique`
56+ FROM `players`
57+ WHERE `online` = 1
58+ ");
59+} else {
60+ $online = db()->fetchOne("
61+ SELECT COUNT(o.player_id) AS `count`, COUNT(DISTINCT p.lastip) AS `unique`
62+ FROM `players_online` o
63+ INNER JOIN `players` p ON o.player_id = p.id
64+ ");
65+}
66+
67+$response['data']['online'] = (int)($online['count'] ?? 0);
68+$response['data']['online_unique_ip'] = (int)($online['unique'] ?? 0);
69+
70+// Server info
71+$response['data']['client'] = $config['client'] ?? null;
72+$response['data']['port'] = $config['port'] ?? null;
73+$response['data']['guildwar'] = $config['guildwar_enabled'] ?? false;
74+$response['data']['forum'] = $config['forum']['enabled'] ?? false;
75+
76+SendResponse($response);
77+?>
A api/module.php +3-0 View file
@@ -0,0 +1,3 @@
1+<?php if (!isset($filepath)) $filepath = '../../../';
2+$moduleVersion = 1;
3+require 'api.php'; ?>
A api/modules/base/player/class/player.php +433-0 View file
@@ -0,0 +1,433 @@
1+<?php
2+
3+class Player {
4+
5+ protected $_playerdata = array(
6+ 'id' => null,
7+ 'name' => null,
8+ 'world_id' => null,
9+ 'group_id' => null,
10+ 'account_id' => null,
11+ 'level' => null,
12+ 'vocation' => null,
13+ 'health' => null,
14+ 'healthmax' => null,
15+ 'experience' => null,
16+ 'lookbody' => null,
17+ 'lookfeet' => null,
18+ 'lookhead' => null,
19+ 'looklegs' => null,
20+ 'looktype' => null,
21+ 'lookaddons' => null,
22+ 'maglevel' => null,
23+ 'mana' => null,
24+ 'manamax' => null,
25+ 'manaspent' => null,
26+ 'soul' => null,
27+ 'town_id' => null,
28+ 'posx' => null,
29+ 'posy' => null,
30+ 'posz' => null,
31+ 'conditions' => null,
32+ 'cap' => null,
33+ 'sex' => null,
34+ 'lastlogin' => null,
35+ 'lastip' => null,
36+ 'save' => null,
37+ 'skull' => null,
38+ 'skulltime' => null,
39+ 'rank_id' => null,
40+ 'guildnick' => null,
41+ 'lastlogout' => null,
42+ 'blessings' => null,
43+ 'balance' => null,
44+ 'stamina' => null,
45+ 'direction' => null,
46+ 'loss_experience' => null,
47+ 'loss_mana' => null,
48+ 'loss_skills' => null,
49+ 'loss_containers' => null,
50+ 'loss_items' => null,
51+ 'premend' => null,
52+ 'online' => null,
53+ 'marriage' => null,
54+ 'promotion' => null,
55+ 'deleted' => null,
56+ 'description' => null,
57+ 'onlinetime' => null,
58+ 'deletion' => null,
59+ 'offlinetraining_time' => null,
60+ 'offlinetraining_skill' => null,
61+ 'skill_fist' => null,
62+ 'skill_fist_tries' => null,
63+ 'skill_club' => null,
64+ 'skill_club_tries' => null,
65+ 'skill_sword' => null,
66+ 'skill_sword_tries' => null,
67+ 'skill_axe' => null,
68+ 'skill_axe_tries' => null,
69+ 'skill_dist' => null,
70+ 'skill_dist_tries' => null,
71+ 'skill_shielding' => null,
72+ 'skill_shielding_tries' => null,
73+ 'skill_fishing' => null,
74+ 'skill_fishing_tries' => null,
75+ );
76+ protected $_znotedata = array(
77+ 'comment' => null,
78+ 'created' => null,
79+ 'hide_char' => null,
80+ );
81+ protected $_name_id = false;
82+ protected $_querylog = array();
83+ protected $_errors = array();
84+
85+ public function __construct(string|int|array $name_id_array, array|string|false $fields = false, bool $query = true) {
86+
87+ if (!is_array($name_id_array)) {
88+ $this->_name_id = $name_id_array;
89+ }
90+
91+ if ($name_id_array !== false) {
92+
93+ if (is_string($name_id_array) || is_int($name_id_array)) {
94+ if ($query) {
95+ $this->update($this->mysql_select($name_id_array, $fields));
96+ }
97+ return;
98+ }
99+
100+ if (is_array($name_id_array)) {
101+ if (isset($name_id_array['id'])) {
102+ $this->_name_id = $name_id_array['id'];
103+ } elseif (isset($name_id_array['name'])) {
104+ $this->_name_id = $name_id_array['name'];
105+ }
106+
107+ $this->update($name_id_array);
108+ return;
109+ }
110+ }
111+
112+ throw new InvalidArgumentException(
113+ 'Player constructor expects string|int|array'
114+ );
115+ }
116+
117+ /**
118+ * Return all player data, or the fields specified in param $fields.
119+ *
120+ * @param array $fields
121+ * @access public
122+ * @return mixed (array 'field' => 'value', or false (bool))
123+ **/
124+ public function fetch($fields = false) {
125+ if (is_string($fields)) {
126+ $fields = [$fields];
127+ }
128+ if ($fields !== false && !is_array($fields)) {
129+ return false;
130+ }
131+
132+ // Return all data that is not null.
133+ if (!$fields) {
134+ $returndata = array();
135+ foreach ($this->_playerdata as $field => $value) {
136+ if (!is_null($value)) $returndata[$field] = $value;
137+ }
138+ foreach ($this->_znotedata as $field => $value) {
139+ if (!is_null($value)) $returndata[$field] = $value;
140+ }
141+ return $returndata;
142+
143+ } else {
144+ // The return array
145+ $returndata = array();
146+
147+ // Array containing null fields, we need to fetch these from db later on.
148+ $missingValues = array();
149+
150+ // Populate the two above arrays
151+ foreach ($fields as $field) {
152+
153+ if (array_key_exists($field, $this->_playerdata)) {
154+ if (is_null($this->_playerdata[$field])) $missingValues[] = $field;
155+ else $returndata[$field] = $this->_playerdata[$field];
156+
157+ } elseif (array_key_exists($field, $this->_znotedata)) {
158+ if (is_null($this->_znotedata[$field])) $missingValues[] = $field;
159+ else $returndata[$field] = $this->_znotedata[$field];
160+ }
161+ }
162+
163+ // See if we are missing any values
164+ if (!empty($missingValues)) {
165+ // Query for this data
166+ $data = $this->mysql_select($this->_name_id, $missingValues);
167+ // Update this object
168+ $this->update($data);
169+ foreach ($data as $field => $value) {
170+ $returndata[$field] = $value;
171+ }
172+ }
173+ return $returndata;
174+ }
175+ return false;
176+ }
177+
178+ /**
179+ * Update player data.
180+ *
181+ * @param array $fields
182+ * @access public
183+ * @return mixed (array, boolean)
184+ **/
185+ public function update(array $data): bool {
186+ if (is_array($data) && !empty($data)) {
187+ foreach ($data as $field => $value) {
188+
189+ if (array_key_exists($field, $this->_playerdata)) {
190+ $this->_playerdata[$field] = $value;
191+
192+ } elseif (array_key_exists($field, $this->_znotedata)) {
193+ $this->_znotedata[$field] = $value;
194+ }
195+ }
196+ return true;
197+ }
198+ return false;
199+ }
200+
201+ public function getErrors() {
202+ return (!empty($this->_errors)) ? $this->_errors : false;
203+ }
204+ public function dumpErrors() {
205+ if ($this->getErrors() !== false)
206+ data_dump($this->getErrors(), false, "Errors detected in player class:");
207+ }
208+
209+ /**
210+ * Select player data from mysql.
211+ *
212+ * @param mixed (int, string) $name_id, array $fields
213+ * @access private
214+ * @return mixed (array, boolean)
215+ **/
216+ private function mysql_select($name_id, $fields = false) {
217+ $table = 'players';
218+ $znote_table = 'znote_players';
219+ $znote_fields = array();
220+
221+ // Dynamic fields logic
222+ switch (gettype($fields)) {
223+ case 'boolean':
224+ $field_elements = '*';
225+ $znote_fields = array('comment', 'created', 'hide_char');
226+ break;
227+
228+ case 'string':
229+ $fields = array($fields);
230+
231+ case 'array':
232+ // Get rid of fields related to znote_
233+ foreach ($fields as $key => $field) {
234+ if (!array_key_exists($field, $this->_playerdata)) {
235+ $znote_fields[] = $field;
236+ unset($fields[$key]);
237+ }
238+ }
239+
240+ //Since we use for loop later, we need to reindex the array if we unset something.
241+ if (!empty($znote_fields)) $fields = array_values($fields);
242+
243+ // Add 'id' field if its not already there.
244+ if (!in_array('id', $fields)) $fields[] = 'id';
245+
246+ // Loop through every field and generate the sql string
247+ $allowedFields = array_keys($this->_playerdata + $this->_znotedata);
248+
249+ $safeFields = [];
250+
251+ foreach ($fields as $field) {
252+ if (!in_array($field, $allowedFields, true)) {
253+ continue;
254+ }
255+ $safeFields[] = "`$field`";
256+ }
257+ if (empty($safeFields)) {
258+ return false;
259+ }
260+ $field_elements = implode(', ', $safeFields);
261+ break;
262+ }
263+
264+ // Value logic
265+ $params = [];
266+ if (is_int($name_id)) {
267+ $name_id = (int)$name_id;
268+ $where = "`id` = ?";
269+ $params[] = $name_id;
270+ } else {
271+ $name_id = getValue($name_id);
272+ if ($name_id === false) {
273+ return false;
274+ }
275+ $where = "`name` = ?";
276+ $params[] = $name_id;
277+ }
278+
279+ $query = "SELECT {$field_elements} FROM `{$table}` WHERE {$where} LIMIT 1;";
280+
281+ // Log query to player object
282+ $this->_querylog[] = $query;
283+ // Fetch from players table
284+ $data = db()->fetchOne($query, $params);
285+ if ($data === false) {
286+ return false;
287+ }
288+
289+ unset($data['conditions']);
290+
291+ // Fetch from znote_players table if neccesary
292+ if (!empty($znote_fields)) {
293+ // Only allow known znote_players columns - the caller-supplied
294+ // field list is not otherwise validated before reaching here.
295+ $znoteAllowed = array_keys($this->_znotedata);
296+ $safeZnoteFields = [];
297+ foreach ($znote_fields as $zf) {
298+ if (in_array($zf, $znoteAllowed, true)) {
299+ $safeZnoteFields[] = "`{$zf}`";
300+ }
301+ }
302+
303+ if (!empty($safeZnoteFields)) {
304+ $field_elements = implode(', ', $safeZnoteFields);
305+ $query = "SELECT {$field_elements} FROM `{$znote_table}` WHERE `player_id` = ? LIMIT 1;";
306+ $this->_querylog[] = $query;
307+ $zdata = db()->fetchOne($query, [$data['id']]);
308+ if (is_array($zdata)) {
309+ foreach ($zdata as $field => $value) $data[$field] = $value;
310+ }
311+ }
312+ }
313+ return $data;
314+ }
315+
316+ /**
317+ * Create player.
318+ *
319+ * @param none
320+ * @access public
321+ * @return bool $status
322+ **/
323+ public function create(int $accountId): bool {
324+ // Player already exists
325+ if (!is_null($this->_playerdata['id'])) {
326+ $this->_errors[] = 'Player already exists.';
327+ return false;
328+ }
329+
330+ // 🔐 SECURE POST ACCESS
331+ $name = $_POST['name'] ?? null;
332+ $vocation = $_POST['selected_vocation'] ?? null;
333+ $town = $_POST['selected_town'] ?? null;
334+ $gender = $_POST['selected_gender'] ?? null;
335+
336+ if (!$name || !$vocation || !$town || $gender === null) {
337+ $this->_errors[] = 'Missing character creation data.';
338+ return false;
339+ }
340+
341+ // Format & validate name
342+ $name = format_character_name($name);
343+ $name = validate_name($name);
344+ $name = sanitize($name);
345+
346+ if ($name === false) {
347+ $this->_errors[] = 'Invalid character name.';
348+ return false;
349+ }
350+
351+ // Check name exists
352+ $exist = db()->fetchOne(
353+ "SELECT `id` FROM `players` WHERE `name` = ? LIMIT 1;", [$name]
354+ );
355+ if ($exist !== false) {
356+ $this->_errors[] = "Character name already exists.";
357+ return false;
358+ }
359+
360+ $config = fullConfig();
361+
362+ // Validate vocation
363+ if (!in_array((int)$vocation, $config['available_vocations'], true)) {
364+ $this->_errors[] = 'Invalid vocation.';
365+ }
366+
367+ // Validate town
368+ if (!in_array((int)$town, $config['available_towns'], true)) {
369+ $this->_errors[] = 'Invalid town.';
370+ }
371+
372+ // Validate gender
373+ if (!in_array((int)$gender, [0, 1], true)) {
374+ $this->_errors[] = 'Invalid gender.';
375+ }
376+
377+ // Stop if errors
378+ if (!empty($this->_errors)) {
379+ return false;
380+ }
381+
382+ // Character count
383+ $char_count = user_character_list_count($accountId);
384+ if ($char_count >= $config['max_characters']) {
385+ $this->_errors[] = 'Maximum characters reached.';
386+ return false;
387+ }
388+
389+ // Prepare insert data
390+ $character_data = [
391+ 'name' => $name,
392+ 'account_id' => $accountId,
393+ 'vocation' => (int)$vocation,
394+ 'town_id' => (int)$town,
395+ 'sex' => (int)$gender,
396+ 'lastip' => getIPLong(),
397+ 'created' => time()
398+ ];
399+
400+ array_walk($character_data, 'array_sanitize');
401+
402+ // Outfit
403+ $character_data['looktype'] = (
404+ $gender == 1
405+ ? $config['maleOutfitId']
406+ : $config['femaleOutfitId']
407+ );
408+
409+ // INSERT PLAYER
410+ db()->execute(
411+ "INSERT INTO `players`
412+ (`name`,`account_id`,`vocation`,`town_id`,`sex`,`lastip`,`created`,`looktype`)
413+ VALUES
414+ (?, ?, ?, ?, ?, " . sqlIpWrite($character_data['lastip']) . ", ?, ?)",
415+ [
416+ $character_data['name'],
417+ $character_data['account_id'],
418+ $character_data['vocation'],
419+ $character_data['town_id'],
420+ $character_data['sex'],
421+ $character_data['created'],
422+ $character_data['looktype'],
423+ ]
424+ );
425+ return true;
426+ }
427+}
428+
429+/*
430+$this->_file = $file . self::EXT;
431+$this->setExpiration(config('cache_lifespan'));
432+$this->_lifespan = $span;
433+*/
A api/modules/base/player/test.php +19-0 View file
@@ -0,0 +1,19 @@
1+<?php $filepath = '../../../../'; require_once '../../../module.php';
2+
3+$response['version']['module'] = 1;
4+
5+UseClass('player');
6+
7+$player = new Player(1129);
8+$data = $player->fetch(['name', 'level']);
9+
10+if ($data === false) {
11+ $response['error'] = 'Player not found';
12+} else {
13+ $response['player'] = $data['name'];
14+ $response['test'] = $data['level'];
15+}
16+
17+SendResponse($response);
18+
19+?>
A api/modules/character/info.php +63-0 View file
@@ -0,0 +1,63 @@
1+<?php
2+require_once '../../module.php';
3+
4+// Module version
5+$response['version']['module'] = 1;
6+
7+// Secure GET name
8+$name = getValue($_GET['name'] ?? null);
9+
10+if ($name === false || $name === '') {
11+ $response['error'] = 'Missing name parameter.';
12+ SendResponse($response);
13+ exit;
14+}
15+
16+$id = user_character_id($name);
17+if ($id === false) {
18+ $response['error'] = 'Character not found.';
19+ SendResponse($response);
20+ exit;
21+}
22+
23+// Respect the same "hide my character" flag the rest of the site honours.
24+if (user_character_hide($name)) {
25+ $response['error'] = 'This character is hidden.';
26+ SendResponse($response);
27+ exit;
28+}
29+
30+UseClass('player');
31+
32+$player = new Player($id);
33+$data = $player->fetch([
34+ 'name', 'level', 'vocation', 'sex',
35+ 'looktype', 'lookhead', 'lookbody', 'looklegs', 'lookfeet', 'lookaddons',
36+ 'health', 'healthmax', 'mana', 'manamax', 'soul', 'cap',
37+ 'experience', 'maglevel', 'town_id',
38+ 'lastlogin', 'lastlogout', 'online', 'created', 'comment',
39+]);
40+
41+if ($data === false) {
42+ $response['error'] = 'Character not found.';
43+ SendResponse($response);
44+ exit;
45+}
46+
47+$data['vocation_name'] = vocation_id_to_name((int)($data['vocation'] ?? -1));
48+
49+$townId = (int)($data['town_id'] ?? 0);
50+$data['town_name'] = $config['towns'][$townId] ?? null;
51+
52+$guild = get_player_guild_data($id);
53+if ($guild !== false) {
54+ $data['guild'] = [
55+ 'name' => get_guild_name((int)$guild['guild_id']) ?: null,
56+ 'rank_name' => $guild['rank_name'] ?? null,
57+ ];
58+}
59+
60+$response['data'] = $data;
61+
62+SendResponse($response);
63+?>
A api/modules/highscores/top.php +65-0 View file
@@ -0,0 +1,65 @@
1+<?php
2+require_once '../../module.php';
3+
4+// Module version
5+$response['version']['module'] = 1;
6+
7+function highscoresTopSkillName(int $type): string {
8+ $types = [
9+ 1 => 'Club', 2 => 'Sword', 3 => 'Axe', 4 => 'Distance', 5 => 'Shield',
10+ 6 => 'Fish', 7 => 'Experience', 8 => 'Magic Level', 9 => 'Fist',
11+ ];
12+ return $types[$type] ?? 'Experience';
13+}
14+
15+$type = isset($_GET['type']) ? (int)getValue($_GET['type'] ?? null) : 7;
16+if ($type > 9 || $type < 1) $type = 7;
17+
18+$configVocations = $config['vocations'];
19+$vocationIds = array_keys($configVocations);
20+
21+$vocation = 'all';
22+if (isset($_GET['vocation']) && is_numeric($_GET['vocation'])) {
23+ $vocation = (int)$_GET['vocation'];
24+ if (!in_array($vocation, $vocationIds, true)) {
25+ $vocation = 'all';
26+ }
27+}
28+
29+$highscore = $config['highscore'];
30+
31+$rows = isset($_GET['rows']) && is_numeric($_GET['rows'])
32+ ? max(1, (int)$_GET['rows'])
33+ : (int)$highscore['rows'];
34+$rows = min($rows, 100);
35+
36+$loadFlags = ($config['country_flags']['enabled'] && $config['country_flags']['highscores']) ? true : false;
37+$loadOutfits = ($config['show_outfits']['highscores']) ? true : false;
38+
39+$defaultRows = (int) $highscore['rows'];
40+$cache = new Cache('engine/cache/highscores');
41+if ($rows === $defaultRows && !$cache->hasExpired()) {
42+ $vocGroups = $cache->load();
43+} else {
44+ $vocGroups = fetchAllScores($rows, znote_server_adapter()->normalizedEngine(), $highscore['ignoreGroupId'], $configVocations, $vocation, $loadFlags, $loadOutfits);
45+ if ($rows === $defaultRows) {
46+ $cache->setContent($vocGroups);
47+ $cache->save();
48+ }
49+}
50+
51+$vocGroup = [];
52+if ($vocGroups) {
53+ $vocGroup = is_array($vocGroups[$vocation]) ? $vocGroups[$vocation] : $vocGroups[$vocGroups[$vocation]];
54+}
55+
56+$response['config'] = [
57+ 'type' => $type,
58+ 'skill_name' => highscoresTopSkillName($type),
59+ 'vocation' => $vocation,
60+ 'rows' => $rows,
61+];
62+$response['data']['rows'] = $vocGroup[$type] ?? [];
63+
64+SendResponse($response);
65+?>
A api/modules/highscores/topExperience.php +51-0 View file
@@ -0,0 +1,51 @@
1+<?php
2+require_once '../../module.php';
3+
4+// Module version
5+$response['version']['module'] = 1;
6+
7+// Secure GET rows
8+$rows = isset($_GET['rows']) && is_numeric($_GET['rows'])
9+ ? max(1, (int)$_GET['rows'])
10+ : 10;
11+
12+// Hard limit safety (anti abuse)
13+$rows = min($rows, 100);
14+
15+$response['config']['rows'] = $rows;
16+
17+// Fetch players
18+$query = "
19+ SELECT
20+ p.name,
21+ p.level,
22+ p.experience,
23+ p.vocation,
24+ p.lastlogin,
25+ z.created
26+ FROM players AS p
27+ INNER JOIN znote_players AS z
28+ ON p.id = z.player_id
29+ WHERE p.group_id < 2
30+ ORDER BY p.experience DESC
31+ LIMIT ?
32+";
33+
34+$players = db()->fetchAll($query, [$rows]);
35+
36+// Always return array
37+if (!is_array($players)) {
38+ $players = [];
39+}
40+
41+// Add vocation name safely
42+foreach ($players as &$player) {
43+ $vocId = (int)($player['vocation'] ?? -1);
44+ $player['vocation_name'] = $config['vocations'][$vocId] ?? 'Unknown';
45+}
46+
47+$response['data']['players'] = $players;
48+
49+SendResponse($response);
50+
51+?>
A api/modules/samples/blank.php +44-0 View file
@@ -0,0 +1,44 @@
1+<?php require_once '../../module.php';
2+// Blank/empty module, nice code to start with when making custom stuff.
3+
4+// Configure module version number
5+$response['version']['module'] = 1;
6+
7+/* Do PHP logic, you got access to:
8+ -Znote AAC sql functions (prepared, use ? placeholders):
9+ :db()->fetchOne($sql, $params), db()->fetchAll($sql, $params)
10+ :db()->execute($sql, $params) for INSERT/UPDATE/DELETE
11+ :db()->transaction(function ($db) { ... }) for several writes, all or nothing
12+
13+ -Config values
14+ :etc $config['vocations']
15+
16+ -Cache system
17+ :Sample:
18+ $cache = new Cache('engine/cache/api/ApiModuleName');
19+ if ($cache->hasExpired()) {
20+ $players = db()->fetchAll("SELECT `name`, `level`, `experience` FROM `players` ORDER BY `experience` DESC LIMIT 5;");
21+
22+ $cache->setContent($players);
23+ $cache->save();
24+ } else {
25+ $players = $cache->load();
26+ }
27+
28+ -Functions found in general.php
29+ :When fetching GET or POST from parameters, ALWAYS use getValue($value)
30+ :Etc if you want to fetch character name from url, do it like this:
31+ $playername = getValue($_GET['name'] ?? null);
32+ if ($playername !== false) {
33+ // $playername either contains player name, or false if failed to fetch name from GET.
34+ }
35+ :getValue is often used in 3 ways: Fetch GET and POST values, or sanitize/secure any value you wish.
36+ :Check ZnoteAAC\engine\function\general.php for full list of available functions.
37+*/
38+
39+// Save the results of previous logic to the response
40+$response['data']['title'] = "The fabulous blank page!";
41+
42+// Send the response through JSON API
43+SendResponse($response);
44+?>
A api/modules/status/online.php +36-0 View file
@@ -0,0 +1,36 @@
1+<?php
2+require_once '../../module.php';
3+
4+// Module version
5+$response['version']['module'] = 1;
6+
7+if (znote_server_adapter()->normalizedEngine() !== 'TFS_10') {
8+ $players = db()->fetchAll("
9+ SELECT `name`, `level`, `vocation`
10+ FROM `players`
11+ WHERE `online` = 1
12+ ORDER BY `level` DESC
13+ ");
14+} else {
15+ $players = db()->fetchAll("
16+ SELECT `p`.`name`, `p`.`level`, `p`.`vocation`
17+ FROM `players_online` `o`
18+ INNER JOIN `players` `p` ON `o`.`player_id` = `p`.`id`
19+ ORDER BY `p`.`level` DESC
20+ ");
21+}
22+
23+if (!is_array($players)) {
24+ $players = [];
25+}
26+
27+foreach ($players as &$player) {
28+ $vocId = (int)($player['vocation'] ?? -1);
29+ $player['vocation_name'] = $config['vocations'][$vocId]['name'] ?? 'Unknown';
30+}
31+unset($player);
32+
33+$response['data']['count'] = count($players);
34+$response['data']['players'] = $players;
35+
36+SendResponse($response);
A api/modules/towns/getTownNames.php +24-0 View file
@@ -0,0 +1,24 @@
1+<?php
2+require_once '../../module.php';
3+
4+// Module version
5+$response['version']['module'] = 1;
6+
7+// Secure config access
8+$towns = $config['towns'] ?? [];
9+$availableTowns = $config['available_towns'] ?? [];
10+
11+// Store all towns
12+$response['data']['towns'] = $towns;
13+
14+// Store available towns only if they exist
15+$response['data']['available'] = [];
16+
17+foreach ($availableTowns as $id) {
18+ if (isset($towns[$id])) {
19+ $response['data']['available'][$id] = $towns[$id];
20+ }
21+}
22+
23+SendResponse($response);
24+?>
A assets/flags/de.png +0-0 View file
Binary file not shown.
A assets/flags/es.png +0-0 View file
Binary file not shown.
A assets/flags/pl.png +0-0 View file
Binary file not shown.
A assets/flags/pt.png +0-0 View file
Binary file not shown.
A assets/flags/uk.png +0-0 View file
Binary file not shown.
Top