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 install/steps/4.php +132-0 View file
@@ -0,0 +1,132 @@
1+<?php
2+/**
3+ * Step 4 - import the ZnoteX schema.
4+ *
5+ * Only the znote_* tables. Every statement in znote_schema.sql is
6+ * CREATE TABLE IF NOT EXISTS, so running this against a database that already
7+ * has some of them adds the missing ones and leaves the rest alone.
8+ */
9+
10+if (!defined('ZNOTE_INSTALL')) { http_response_code(403); die('Direct access denied.'); }
11+
12+$schemaFile = install_root() . '/SQL/znote_schema.sql';
13+$report = null;
14+
15+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
16+
17+ $link = install_connect($connectError);
18+
19+ if ($link === null) {
20+ install_error('Lost the database connection: ' . ih((string)$connectError));
21+ } elseif (!is_file($schemaFile)) {
22+ install_error('SQL/znote_schema.sql is missing.');
23+ } else {
24+
25+ $sql = (string)file_get_contents($schemaFile);
26+ $sql = str_replace(array("\r\n", "\r"), "\n", $sql);
27+
28+ // Strip line comments, then split on ";" at end of line. The file is
29+ // generated by us and has no procedures or triggers, so this is safe -
30+ // a general-purpose SQL splitter would be overkill.
31+ $sql = preg_replace('/^--.*$/m', '', $sql);
32+
33+ $created = 0;
34+ $skipped = 0;
35+ $errors = array();
36+
37+ foreach (array_filter(array_map('trim', explode(";\n", $sql))) as $statement) {
38+ $statement = trim($statement, "; \t\n\r");
39+ if ($statement === '') {
40+ continue;
41+ }
42+
43+ // Seed rows must not be duplicated on a re-run.
44+ if (stripos($statement, 'INSERT INTO') === 0) {
45+ if (preg_match('/INSERT INTO `([a-z_]+)`/i', $statement, $m)) {
46+ $check = @$link->query('SELECT 1 FROM `' . $m[1] . '` LIMIT 1');
47+ if ($check !== false && $check->num_rows > 0) {
48+ $skipped++;
49+ continue;
50+ }
51+ }
52+ }
53+
54+ if (@$link->query($statement)) {
55+ $created++;
56+ } else {
57+ $errors[] = $link->error . "\n in: "
58+ . substr((string)preg_replace('/\s+/', ' ', $statement), 0, 90);
59+ }
60+ }
61+
62+ // Record the version, which the schema's own INSERT may have skipped.
63+ @$link->query("UPDATE `znote` SET `version` = '2.0.1'");
64+
65+ $tablesNow = 0;
66+ $result = @$link->query("SHOW TABLES LIKE 'znote%'");
67+ if ($result !== false) {
68+ $tablesNow = $result->num_rows;
69+ }
70+
71+ $link->close();
72+
73+ $report = array(
74+ 'created' => $created,
75+ 'skipped' => $skipped,
76+ 'errors' => $errors,
77+ 'tables' => $tablesNow,
78+ );
79+
80+ if (!$errors) {
81+ install_state(array('schema_done' => true));
82+ install_max_step(5);
83+ } else {
84+ install_error(count($errors) . ' statement(s) failed. The first was: '
85+ . ih((string)$errors[0]));
86+ }
87+ }
88+}
89+?>
90+<h1>Schema</h1>
91+<p class="lead">
92+ Creates the <code>znote_*</code> tables in
93+ <code><?= ih(install_get('sqlDatabase')) ?></code>. Your server's own tables are not touched.
94+</p>
95+
96+<?php if ($report === null): ?>
97+
98+ <div class="info">
99+ Every statement is <code>CREATE TABLE IF NOT EXISTS</code>, and seed rows are skipped for
100+ tables that already hold data &mdash; running this on a database that already has some
101+ ZnoteX tables adds only what is missing.
102+ </div>
103+
104+ <form method="post">
105+ <div class="actions">
106+ <button class="btn" type="submit">Import the schema</button>
107+ <a class="btn ghost" href="<?= install_url(3) ?>">Back</a>
108+ </div>
109+ </form>
110+
111+<?php else: ?>
112+
113+ <?php if (!$report['errors']): ?>
114+ <p class="good">
115+ Done. <?= (int)$report['created'] ?> statement<?= $report['created'] === 1 ? '' : 's' ?> ran,
116+ <?= (int)$report['skipped'] ?> skipped as already populated.
117+ <?= (int)$report['tables'] ?> <code>znote_*</code> tables are now present.
118+ </p>
119+ <?php else: ?>
120+ <p class="bad"><?= count($report['errors']) ?> statement(s) failed:</p>
121+ <pre><?php foreach (array_slice($report['errors'], 0, 6) as $e) { echo ih($e), "\n"; } ?></pre>
122+ <?php endif; ?>
123+
124+ <div class="actions">
125+ <?php if (!$report['errors']): ?>
126+ <a class="btn" href="<?= install_url(5) ?>">Continue</a>
127+ <?php else: ?>
128+ <a class="btn ghost" href="<?= install_url(4) ?>">Try again</a>
129+ <?php endif; ?>
130+ </div>
131+
132+<?php endif; ?>
A install/steps/5.php +187-0 View file
@@ -0,0 +1,187 @@
1+<?php
2+/**
3+ * Step 5 - the administrator.
4+ *
5+ * Creates an account, a character on it, and remembers the account name so the
6+ * last step can put it in page_admin_access.
7+ *
8+ * The account is created the way register.php does it, so the password hash
9+ * matches what login.php expects on this engine.
10+ */
11+
12+if (!defined('ZNOTE_INSTALL')) { http_response_code(403); die('Direct access denied.'); }
13+
14+$engine = (string)install_get('ServerEngine', 'TFS_10');
15+$isOthire = ($engine === 'OTHIRE');
16+$done = (bool)install_get('admin_done', false);
17+
18+if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$done) {
19+
20+ $accountName = trim((string)($_POST['account'] ?? ''));
21+ $password = (string)($_POST['password'] ?? '');
22+ $password2 = (string)($_POST['password_again'] ?? '');
23+ $email = trim((string)($_POST['email'] ?? ''));
24+ $character = trim((string)($_POST['character'] ?? ''));
25+
26+ $problems = array();
27+
28+ if ($accountName === '' || strlen($accountName) > 30) { $problems[] = 'The account name is required, up to 30 characters.'; }
29+ if ($isOthire) {
30+ if (!preg_match('/^[0-9]+$/', $accountName)) {
31+ $problems[] = 'OTHire account numbers may only contain digits.';
32+ }
33+ } elseif (!preg_match('/^[A-Za-z0-9]+$/', $accountName)) {
34+ $problems[] = 'The account name may only contain letters and numbers. Do not use @ or special characters.';
35+ }
36+ if (strlen($password) < 6) { $problems[] = 'The password must be at least 6 characters.'; }
37+ if (strlen($password) > 29) { $problems[] = 'The password may not be longer than 29 characters.'; }
38+ if ($password !== $password2) { $problems[] = 'The passwords do not match.'; }
39+ if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) { $problems[] = 'A valid e-mail address is required.'; }
40+ if ($character === '' || strlen($character) > 20) { $problems[] = 'The character name is required, up to 20 characters.'; }
41+ if (!preg_match('/^[A-Za-z ]+$/', $character)) { $problems[] = 'The character name may only contain letters and spaces.'; }
42+
43+ if ($problems) {
44+ install_error(implode('<br>', array_map('ih', $problems)));
45+ } else {
46+
47+ $link = install_connect($connectError);
48+
49+ if ($link === null) {
50+ install_error('Lost the database connection: ' . ih((string)$connectError));
51+ } else {
52+
53+ $esc = static fn(string $v): string => $link->real_escape_string($v);
54+
55+ // Refuse rather than silently attach to someone else's account.
56+ $taken = @$link->query("SELECT `id` FROM `players` WHERE `name` = '" . $esc($character) . "' LIMIT 1");
57+ if ($taken !== false && $taken->num_rows > 0) {
58+ install_error('A character named <strong>' . ih($character) . '</strong> already exists.');
59+ } else {
60+
61+ $now = time();
62+ $hash = sha1($password);
63+
64+ if ($isOthire) {
65+ // OTHire identifies accounts by number, not by name.
66+ $accountId = (int)$accountName;
67+ @$link->query("INSERT INTO `accounts` (`id`, `password`, `email`) VALUES ({$accountId}, '{$hash}', '" . $esc($email) . "')");
68+ } else {
69+ $creation = ($engine === 'TFS_10' || $engine === 'TFS_16' || $engine === 'CANARY')
70+ ? ", `creation`" : '';
71+ $creationValue = $creation !== '' ? ", {$now}" : '';
72+
73+ @$link->query("INSERT INTO `accounts` (`name`, `password`, `email`{$creation})
74+ VALUES ('" . $esc($accountName) . "', '{$hash}', '" . $esc($email) . "'{$creationValue})");
75+ $accountId = (int)$link->insert_id;
76+ }
77+
78+ if ($accountId <= 0) {
79+ install_error('Could not create the account: ' . ih($link->error));
80+ } else {
81+
82+ @$link->query("INSERT INTO `znote_accounts` (`account_id`, `ip`, `created`, `points`, `active`, `active_email`, `activekey`, `flag`)
83+ VALUES ({$accountId}, 0, {$now}, 0, 1, 1, 0, '')");
84+
85+ // A level 8 knight with the stock starting stats. The point
86+ // is to have a character whose name grants panel access;
87+ // tune it in game or from Admin Panel > Character Skills.
88+ $ok = @$link->query("INSERT INTO `players`
89+ (`name`, `group_id`, `account_id`, `level`, `vocation`, `health`, `healthmax`,
90+ `experience`, `maglevel`, `mana`, `manamax`, `town_id`, `cap`, `sex`, `looktype`)
91+ VALUES ('" . $esc($character) . "', 1, {$accountId}, 8, 4, 185, 185, 4200, 0, 90, 90, 1, 470, 1, 128)");
92+
93+ if (!$ok) {
94+ install_error('The account was created but the character was not: ' . ih($link->error)
95+ . '<br>Your server\'s <code>players</code> table may need columns this insert does not set.');
96+ } else {
97+ $playerId = (int)$link->insert_id;
98+ @$link->query("INSERT INTO `znote_players` (`player_id`, `created`, `hide_char`, `comment`)
99+ VALUES ({$playerId}, {$now}, 0, '')");
100+
101+ install_state(array(
102+ 'admin_done' => true,
103+ 'admin_account' => $accountName,
104+ 'admin_character' => $character,
105+ ));
106+ install_max_step(6);
107+
108+ $link->close();
109+ header('Location: ' . install_url(6));
110+ exit;
111+ }
112+ }
113+ }
114+
115+ $link->close();
116+ }
117+ }
118+}
119+?>
120+<h1>Administrator</h1>
121+
122+<?php if ($done): ?>
123+
124+ <p class="good">
125+ Account <strong><?= ih(install_get('admin_account')) ?></strong> and character
126+ <strong><?= ih(install_get('admin_character')) ?></strong> were created. The account
127+ is given panel access at the next step.
128+ </p>
129+ <div class="actions">
130+ <a class="btn" href="<?= install_url(6) ?>">Continue</a>
131+ </div>
132+
133+<?php else: ?>
134+
135+ <p class="lead">
136+ An account to log in with, and a character on it. ZnoteX grants admin rights by
137+ <em>account name</em>, so the account name below is what unlocks the panel.
138+ </p>
139+
140+ <form method="post">
141+ <div class="row">
142+ <div class="field">
143+ <label class="lbl" for="account"><?= $isOthire ? 'Account number' : 'Account name' ?></label>
144+ <input type="text" id="account" name="account" maxlength="30"
145+ value="<?= ih(install_get('admin_account')) ?>" required>
146+ <?php if ($isOthire): ?>
147+ <p class="hint">OTHire identifies accounts by number.</p>
148+ <?php else: ?>
149+ <p class="hint">Letters and numbers only. Do not use @ or special characters.</p>
150+ <?php endif; ?>
151+ </div>
152+ <div class="field">
153+ <label class="lbl" for="email">E-mail</label>
154+ <input type="email" id="email" name="email" required>
155+ </div>
156+ </div>
157+
158+ <div class="row">
159+ <div class="field">
160+ <label class="lbl" for="password">Password</label>
161+ <input type="password" id="password" name="password" required>
162+ <p class="hint">At least 6 characters.</p>
163+ </div>
164+ <div class="field">
165+ <label class="lbl" for="password_again">Password again</label>
166+ <input type="password" id="password_again" name="password_again" required>
167+ </div>
168+ </div>
169+
170+ <div class="field">
171+ <label class="lbl" for="character">Character name</label>
172+ <input type="text" id="character" name="character" maxlength="20" required>
173+ <p class="hint">Letters and spaces only. Your character in game &mdash; panel access comes from the account name above.</p>
174+ </div>
175+
176+ <div class="info">
177+ The character is created as a level 8 knight with the default starting stats. Change it
178+ in game, or from <strong>Admin Panel &rarr; Character Skills</strong>, once you are in.
179+ </div>
180+
181+ <div class="actions">
182+ <button class="btn" type="submit">Create the administrator</button>
183+ <a class="btn ghost" href="<?= install_url(4) ?>">Back</a>
184+ </div>
185+ </form>
186+
187+<?php endif; ?>
A install/steps/6.php +157-0 View file
@@ -0,0 +1,157 @@
1+<?php
2+/**
3+ * Step 6 - write the configuration and lock the installer.
4+ *
5+ * The generated values go to config.local.php, which config.php includes at
6+ * the end. config.php is never rewritten: it stays the documented default file
7+ * you can update with a new ZnoteX release without losing your settings, and
8+ * a 1000-line file edited by regular expression is a file waiting to break.
9+ */
10+
11+if (!defined('ZNOTE_INSTALL')) { http_response_code(403); die('Direct access denied.'); }
12+
13+/**
14+ * The file we are about to write, also shown for manual copying.
15+ */
16+function install_config_contents(): string {
17+ $quote = static function (string $value): string {
18+ return "'" . str_replace(array('\\', "'"), array('\\\\', "\\'"), $value) . "'";
19+ };
20+
21+ $admin = (string)install_get('admin_account');
22+ $nl = "\n";
23+
24+ $out = '<?php' . $nl;
25+ $out .= '/**' . $nl;
26+ $out .= ' * Written by the ZnoteX installer.' . $nl;
27+ $out .= ' *' . $nl;
28+ $out .= ' * config.php is included first and holds every default; this file overrides the' . $nl;
29+ $out .= ' * handful of values specific to this install. Updating ZnoteX therefore means' . $nl;
30+ $out .= ' * replacing config.php and leaving this file alone.' . $nl;
31+ $out .= ' *' . $nl;
32+ $out .= ' * Most other settings are editable from Admin Panel > Settings.' . $nl;
33+ $out .= ' */' . $nl . $nl;
34+
35+ $out .= '$config[\'sqlHost\'] = ' . $quote((string)install_get('sqlHost')) . ';' . $nl;
36+ $out .= '$config[\'sqlUser\'] = ' . $quote((string)install_get('sqlUser')) . ';' . $nl;
37+ $out .= '$config[\'sqlPassword\'] = ' . $quote((string)install_get('sqlPassword')) . ';' . $nl;
38+ $out .= '$config[\'sqlDatabase\'] = ' . $quote((string)install_get('sqlDatabase')) . ';' . $nl . $nl;
39+
40+ $out .= '$config[\'ServerEngine\'] = ' . $quote((string)install_get('ServerEngine', 'TFS_10')) . ';' . $nl;
41+ $out .= '$config[\'site_title\'] = ' . $quote((string)install_get('site_title')) . ';' . $nl;
42+ $out .= '$config[\'site_url\'] = ' . $quote((string)install_get('site_url')) . ';' . $nl;
43+
44+ $path = (string)install_get('server_path');
45+ if ($path !== '') {
46+ $out .= '$config[\'server_path\'] = ' . $quote($path) . ';' . $nl;
47+ }
48+
49+ $out .= $nl . '// Admin access is granted by account name.' . $nl;
50+ $out .= '$config[\'page_admin_access\'] = array(' . $nl;
51+ $out .= "\t" . $quote($admin) . ',' . $nl;
52+ $out .= ');' . $nl;
53+
54+ return $out;
55+}
56+
57+$written = false;
58+$writeErr = '';
59+
60+$contents = install_config_contents();
61+
62+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
63+ $target = install_config_file();
64+
65+ if (@file_put_contents($target, $contents) === false) {
66+ $writeErr = 'Could not write ' . $target . '. Create it by hand with the contents below.';
67+ } else {
68+ $written = true;
69+
70+ // The lock. Written last, so a failed write above does not lock a site
71+ // that is not actually configured.
72+ @file_put_contents(install_lock_file(),
73+ "Installed on " . date('Y-m-d H:i:s') . "\n"
74+ . "Delete this file only if you mean to run the installer again.\n");
75+
76+ install_reset();
77+ }
78+}
79+?>
80+<h1><?= $written ? 'Installed' : 'Finish' ?></h1>
81+
82+<?php if ($written): ?>
83+
84+ <p class="good">
85+ ZnoteX is installed. <code>config.local.php</code> is written and the installer is locked.
86+ </p>
87+
88+ <h2>Before you open the site</h2>
89+ <p class="lead">
90+ Vocations, starting towns and starting skills come pre-filled with generic defaults.
91+ Most servers change at least some of this before letting anyone create a character -
92+ it is much less painful to set it now than after players have already made characters.
93+ </p>
94+ <ul class="checks">
95+ <li>
96+ <span class="state opt">1</span>
97+ <span class="what">
98+ Configure vocations, towns &amp; starting skills
99+ <span class="detail">Admin Panel &rarr; Settings &rarr; Character creation. Each one is an editable table now, no JSON to hand-edit.</span>
100+ </span>
101+ </li>
102+ <li>
103+ <span class="state opt">2</span>
104+ <span class="what">
105+ Delete the <code>install/</code> folder
106+ <span class="detail">It refuses to run now, but there is no reason to leave it on a public server.</span>
107+ </span>
108+ </li>
109+ <li>
110+ <span class="state opt">3</span>
111+ <span class="what">
112+ Look around the rest of the panel
113+ <span class="detail">Admin Panel &rarr; Settings covers almost everything else you would otherwise edit by hand.</span>
114+ </span>
115+ </li>
116+ </ul>
117+
118+ <div class="actions">
119+ <a class="btn green" href="../admin/index.php?p=settings#character-creation">Configure vocations &amp; towns</a>
120+ <a class="btn ghost" href="../admin/index.php">Admin panel</a>
121+ <a class="btn ghost" href="../index.php">Open the site</a>
122+ </div>
123+
124+<?php else: ?>
125+
126+ <p class="lead">Review what will be written, then finish.</p>
127+
128+ <table>
129+ <tr><th>Setting</th><th>Value</th></tr>
130+ <tr><td>Database</td><td><code><?= ih(install_get('sqlUser')) ?>@<?= ih(install_get('sqlHost')) ?> / <?= ih(install_get('sqlDatabase')) ?></code></td></tr>
131+ <tr><td>Server engine</td><td><code><?= ih(install_get('ServerEngine')) ?></code></td></tr>
132+ <tr><td>Site name</td><td><?= ih(install_get('site_title')) ?></td></tr>
133+ <tr><td>Site URL</td><td><?= ih(install_get('site_url')) ?></td></tr>
134+ <tr><td>Administrator</td><td><?= ih(install_get('admin_account')) ?> <span class="detail">(account name &mdash; this is what grants panel access)</span></td></tr>
135+ <tr><td>Character</td><td><?= ih(install_get('admin_character')) ?></td></tr>
136+ </table>
137+
138+ <?php if ($writeErr !== ''): ?>
139+ <p class="bad"><?= ih($writeErr) ?></p>
140+ <h2>Create config.local.php yourself</h2>
141+ <p>Put this in the ZnoteX root, next to <code>config.php</code>:</p>
142+ <pre><code><?= ih($contents) ?></code></pre>
143+ <?php endif; ?>
144+
145+ <div class="info">
146+ These go to <code>config.local.php</code>. Your <code>config.php</code> is left untouched,
147+ so a future ZnoteX update can replace it without losing anything.
148+ </div>
149+
150+ <form method="post">
151+ <div class="actions">
152+ <button class="btn green" type="submit">Write the config and finish</button>
153+ <a class="btn ghost" href="<?= install_url(5) ?>">Back</a>
154+ </div>
155+ </form>
156+
157+<?php endif; ?>
A ipn.php +228-0 View file
@@ -0,0 +1,228 @@
1+<?php
2+/* 2021: Paypal hosts arent neccesarily notify.paypal.com any longer.
3+if (gethostbyaddr($_SERVER['REMOTE_ADDR']) !== 'notify.paypal.com') {
4+ exit();
5+}
6+*/
7+
8+function ip_in_range( $ip, $range ) {
9+ if ( strpos( $range, '/' ) === false ) {
10+ $range .= '/32';
11+ }
12+ // $range is in IP/CIDR format eg 127.0.0.1/24
13+ list( $range, $netmask ) = explode( '/', $range, 2 );
14+ $range_decimal = ip2long( $range );
15+ $ip_decimal = ip2long( $ip );
16+ $wildcard_decimal = pow( 2, ( 32 - (int)$netmask ) ) - 1;
17+ $netmask_decimal = ~ $wildcard_decimal;
18+ return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
19+}
20+
21+$paypal_ip_ranges = array(
22+ "173.0.81.65",
23+ "173.0.81.140",
24+ "64.4.240.0/21",
25+ "64.4.248.0/22",
26+ "66.211.168.0/22",
27+ "173.0.80.0/20",
28+ "91.243.72.0/23"
29+);
30+
31+$verified = false;
32+for($i = 0; $i < count($paypal_ip_ranges); $i++) {
33+ if(ip_in_range($_SERVER["REMOTE_ADDR"], $paypal_ip_ranges[$i])) {
34+ $verified = true;
35+ break;
36+ }
37+}
38+
39+if(!$verified) {
40+ exit();
41+}
42+
43+// Require the functions to connect to database and fetch config values
44+require 'config.php';
45+require 'engine/database/connect.php';
46+
47+// Fetch and sanitize POST and GET values
48+function getValue($value) {
49+ return (!empty($value)) ? sanitize($value) : false;
50+}
51+function sanitize($data) {
52+ return htmlentities(strip_tags(mysql_znote_escape_string($data)));
53+}
54+
55+require_once 'engine/function/translate.php';
56+require_once 'engine/function/settings.php';
57+require_once 'engine/function/users.php';
58+require_once 'engine/function/plugins.php';
59+znote_apply_settings();
60+znote_plugins_load();
61+
62+function VerifyPaypalIPN(?array $IPN = null){
63+ if(empty($IPN)){
64+ $IPN = $_POST;
65+ }
66+ if(empty($IPN['verify_sign'])){
67+ return null;
68+ }
69+ $IPN['cmd'] = '_notify-validate';
70+ $PaypalHost = (empty($IPN['test_ipn']) ? 'www' : 'www.sandbox').'.paypal.com';
71+ $cURL = curl_init();
72+ curl_setopt($cURL, CURLOPT_SSL_VERIFYPEER, 1);
73+ curl_setopt($cURL, CURLOPT_SSL_VERIFYHOST, 2);
74+ curl_setopt($cURL, CURLOPT_SSLVERSION, 6);
75+ curl_setopt($cURL, CURLOPT_CAINFO, __DIR__ . '/engine/cert/cacert.pem');
76+ curl_setopt($cURL, CURLOPT_URL, "https://{$PaypalHost}/cgi-bin/webscr");
77+ curl_setopt($cURL, CURLOPT_ENCODING, 'gzip');
78+ curl_setopt($cURL, CURLOPT_POST, true); // POST back
79+ curl_setopt($cURL, CURLOPT_POSTFIELDS, $IPN); // the $IPN
80+ curl_setopt($cURL, CURLOPT_HEADER, false);
81+ curl_setopt($cURL, CURLOPT_RETURNTRANSFER, true);
82+ curl_setopt($cURL, CURLOPT_FORBID_REUSE, true);
83+ curl_setopt($cURL, CURLOPT_FRESH_CONNECT, true);
84+ curl_setopt($cURL, CURLOPT_CONNECTTIMEOUT, 30);
85+ curl_setopt($cURL, CURLOPT_TIMEOUT, 60);
86+ curl_setopt($cURL, CURLINFO_HEADER_OUT, true);
87+ curl_setopt($cURL, CURLOPT_HTTPHEADER, array(
88+ 'Connection: close',
89+ 'Expect: ',
90+ ));
91+ $Response = curl_exec($cURL);
92+ $Status = (int)curl_getinfo($cURL, CURLINFO_HTTP_CODE);
93+ curl_close($cURL);
94+ if(empty($Response) or !preg_match('~^(VERIFIED|INVALID)$~i', $Response = trim($Response)) or !$Status){
95+ return null;
96+ }
97+ if(intval($Status / 100) != 2){
98+ return false;
99+ }
100+ return !strcasecmp($Response, 'VERIFIED');
101+}
102+
103+// Fetch paypal configurations
104+$paypal = $config['paypal'];
105+$prices = $config['paypal_prices'];
106+
107+// Send an empty HTTP 204 OK response to acknowledge receipt of the notification
108+http_response_code(204);
109+
110+// Build the required acknowledgement message out of the notification just received
111+$postdata = 'cmd=_notify-validate';
112+if(!empty($_POST)){
113+ $postdata.="&".http_build_query($_POST);
114+}
115+// Assign payment notification values to local variables
116+$item_name = $_POST['item_name'] ?? null;
117+$item_number = $_POST['item_number'] ?? null;
118+$payment_status = $_POST['payment_status'] ?? null;
119+$payment_amount = $_POST['mc_gross'] ?? null;
120+$payment_currency = $_POST['mc_currency'] ?? null;
121+$txn_id = getValue($_POST['txn_id'] ?? null);
122+$receiver_email = getValue($_POST['receiver_email'] ?? null);
123+$payer_email = getValue($_POST['payer_email'] ?? null);
124+$custom_raw = (string)($_POST['custom'] ?? '');
125+$custom = (int)$custom_raw;
126+
127+$connectedIp = $_SERVER['REMOTE_ADDR'];
128+db()->execute("INSERT INTO `znote_paypal` VALUES ('0', '0', ?, '0', '0', '0')", ["Connection from IP: $connectedIp"]);
129+
130+$status = VerifyPaypalIPN();
131+if ($status) {
132+ // Check that the payment_status is Completed
133+ if ($payment_status == 'Completed') {
134+
135+
136+ // Check that txn_id has not been previously processed
137+ $txn_id_check = db()->fetchOne("SELECT `txn_id` FROM `znote_paypal` WHERE `txn_id` = ?", [$txn_id]);
138+ if ($txn_id_check === false) {
139+ // Check that receiver_email is your Primary PayPal email
140+ if ($receiver_email == $paypal['email']) {
141+
142+ $status = true;
143+ $paidMoney = 0;
144+ $paidPoints = 0;
145+ $payment = array(
146+ 'provider' => 'paypal',
147+ 'reference' => (string)$txn_id,
148+ 'custom' => $custom_raw,
149+ 'account_id' => $custom,
150+ 'price' => $payment_amount,
151+ 'currency' => $payment_currency,
152+ 'points' => 0,
153+ 'status' => 'Completed',
154+ 'raw' => $_POST,
155+ 'resolved' => false,
156+ );
157+ if (function_exists('znote_hook_filter')) {
158+ $payment = znote_hook_filter('payment.resolve', $payment, array('provider' => 'paypal', 'raw' => $_POST));
159+ }
160+
161+ if (!empty($payment['resolved'])) {
162+ $custom = (int)($payment['account_id'] ?? 0);
163+ $paidMoney = $payment['price'] ?? 0;
164+ $paidPoints = (int)($payment['points'] ?? 0);
165+ } else {
166+ foreach ($prices as $priceValue => $pointsValue) {
167+ if ($priceValue == $payment_amount) {
168+ $paidMoney = $priceValue;
169+ $paidPoints = $pointsValue;
170+ }
171+ }
172+ }
173+
174+ if ($paidMoney == 0 || number_format((float)$paidMoney, 2, '.', '') !== number_format((float)$payment_amount, 2, '.', '')) $status = false; // Wrong ammount of money
175+ if ($payment_currency != ($payment['currency'] ?? $paypal['currency'])) $status = false; // Wrong currency
176+ if ($custom <= 0) $status = false;
177+
178+ // Verify that the user havent messed around with POST data
179+ if ($status) {
180+ // Re-check for a duplicate and credit inside one locked transaction,
181+ // so two concurrent IPN deliveries for the same txn_id cannot both credit points.
182+ $creditResult = db()->transaction(function ($db) use ($txn_id, $payer_email, $custom, $paidMoney, $paidPoints) {
183+ $dup = $db->fetchOne("SELECT `txn_id` FROM `znote_paypal` WHERE `txn_id` = ? LIMIT 1 FOR UPDATE;", [$txn_id]);
184+ if ($dup !== false) {
185+ return 'duplicate';
186+ }
187+
188+ $db->execute("INSERT INTO `znote_paypal` VALUES ('0', ?, ?, ?, ?, ?)", [$txn_id, $payer_email, $custom, $paidMoney, $paidPoints]);
189+
190+ $data = $db->fetchOne("SELECT `points` AS `old_points` FROM `znote_accounts` WHERE `account_id` = ? LIMIT 1 FOR UPDATE;", [$custom]);
191+ if (!is_array($data)) {
192+ return 'no_account';
193+ }
194+
195+ $new_points = (int)$data['old_points'] + $paidPoints;
196+ $db->execute("UPDATE `znote_accounts` SET `points` = ? WHERE `account_id` = ?", [$new_points, $custom]);
197+
198+ return 'credited';
199+ });
200+
201+ if ($creditResult === 'credited') {
202+ if (function_exists('znote_hook')) {
203+ znote_hook('payment.completed', array_merge($payment, array(
204+ 'provider' => 'paypal',
205+ 'reference' => (string)$txn_id,
206+ 'custom' => $custom_raw,
207+ 'account_id' => $custom,
208+ 'price' => $paidMoney,
209+ 'currency' => $payment_currency,
210+ 'points' => $paidPoints,
211+ 'status' => 'Completed',
212+ )));
213+ }
214+ } elseif ($creditResult === 'no_account') {
215+ db()->execute("INSERT INTO `znote_paypal` VALUES ('0', ?, ?, '0', '0', '0')", [$txn_id, "ERROR: No znote_accounts row for account_id $custom"]);
216+ }
217+ }
218+ } else {
219+ $pmail = $paypal['email'];
220+ db()->execute("INSERT INTO `znote_paypal` VALUES ('0', ?, ?, '0', '0', '0')", [$txn_id, "ERROR: Wrong mail. Received: $receiver_email, configured: $pmail"]);
221+ }
222+ }
223+ }
224+} else {
225+ // Something is wrong
226+ db()->execute("INSERT INTO `znote_paypal` VALUES ('0', ?, ?, '0', '0', '0')", [$txn_id, "ERROR: Invalid data. $postdata"]);
227+}
228+?>
A items.php +30-0 View file
@@ -0,0 +1,30 @@
1+<?php require_once 'engine/init.php'; theme_open();
2+
3+/**
4+ * Equipable items browser.
5+ *
6+ * items.xml is uploaded and parsed in the admin panel, under Server Info. This
7+ * page only reads the result:
8+ * $itemsEnabled the page is turned on in config.php
9+ * $items the item list, or false when nothing was published
10+ * $itemsAdmin, $itemsUpdated, $itemsFailed kept at false for older themes
11+ */
12+
13+$itemsEnabled = ($config['items'] == true);
14+$itemsAdmin = false;
15+$itemsUpdated = false;
16+$itemsFailed = false;
17+$items = false;
18+
19+if ($itemsEnabled) {
20+ $items = serverdata_load('items');
21+
22+ if ($items === false && is_file(serverdata_file('items.xml'))) {
23+ serverdata_rebuild('items');
24+ $items = serverdata_load('items');
25+ }
26+}
27+
28+view('items');
29+
30+theme_close();
A killers.php +73-0 View file
@@ -0,0 +1,73 @@
1+<?php
2+require_once 'engine/init.php';
3+theme_open();
4+
5+/**
6+ * Kill statistics.
7+ *
8+ * TFS 0.3 exposes a different set of tables from the 1.x line, so the two
9+ * engines produce different data. Which one applies is decided here and the
10+ * view only has to read $killersMode - it never touches the database.
11+ */
12+
13+$engine = znote_server_adapter()->normalizedEngine();
14+
15+if (in_array($engine, array('TFS_02', 'TFS_10', 'OTHIRE'), true)) {
16+ $killersMode = 'modern';
17+} elseif ($engine === 'TFS_03') {
18+ $killersMode = 'legacy';
19+} else {
20+ $killersMode = 'unsupported';
21+}
22+
23+$killers = false;
24+$victims = false;
25+$latests = false;
26+$deaths = false;
27+
28+if ($killersMode === 'modern') {
29+
30+ $cache = new Cache('engine/cache/killers');
31+ if ($cache->hasExpired()) {
32+ $killers = fetchMurders();
33+ $cache->setContent($killers);
34+ $cache->save();
35+ } else {
36+ $killers = $cache->load();
37+ }
38+
39+ $cache = new Cache('engine/cache/victims');
40+ if ($cache->hasExpired()) {
41+ $victims = fetchLoosers();
42+ $cache->setContent($victims);
43+ $cache->save();
44+ } else {
45+ $victims = $cache->load();
46+ }
47+
48+ $cache = new Cache('engine/cache/lastkillers');
49+ if ($cache->hasExpired()) {
50+ $latests = db()->fetchAll("SELECT `p`.`name` AS `victim`, `d`.`killed_by` as `killed_by`, `d`.`time` as `time` FROM `player_deaths` as `d` INNER JOIN `players` as `p` ON d.player_id = p.id WHERE d.`is_player`='1' ORDER BY `time` DESC LIMIT 20;");
51+ if ($latests !== false) {
52+ $cache->setContent($latests);
53+ $cache->save();
54+ }
55+ } else {
56+ $latests = $cache->load();
57+ }
58+
59+} elseif ($killersMode === 'legacy') {
60+
61+ $cache = new Cache('engine/cache/killers');
62+ if ($cache->hasExpired()) {
63+ $deaths = fetchLatestDeaths_03(30, true);
64+ $cache->setContent($deaths);
65+ $cache->save();
66+ } else {
67+ $deaths = $cache->load();
68+ }
69+}
70+
71+view('killers');
72+
73+theme_close();
A landing/css/landing.css +30-0 View file
@@ -0,0 +1,30 @@
1+:root { color-scheme: dark; }
2+
3+body {
4+ margin: 0;
5+ min-height: 100vh;
6+ display: grid;
7+ place-items: center;
8+ padding: 24px;
9+ background: #14181f;
10+ color: #dfe4ec;
11+ font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
12+}
13+
14+main { max-width: 560px; text-align: center; }
15+h1 { font-size: 28px; margin: 0 0 16px; }
16+p { color: #93a0b2; margin: 0 0 16px; }
17+.note { font-size: 14px; }
18+code { color: #d1a233; }
19+
20+.enter {
21+ display: inline-block;
22+ margin-top: 8px;
23+ padding: 12px 28px;
24+ color: #14181f;
25+ background: #d1a233;
26+ border-radius: 4px;
27+ font-weight: 600;
28+ text-decoration: none;
29+}
30+.enter:hover { background: #e2b854; }
A landing/index.html +20-0 View file
@@ -0,0 +1,20 @@
1+<!doctype html>
2+<html lang="en">
3+<head>
4+ <meta charset="utf-8">
5+ <meta name="viewport" content="width=device-width, initial-scale=1">
6+ <title>Welcome</title>
7+ <link rel="stylesheet" href="css/landing.css">
8+</head>
9+<body>
10+ <main>
11+ <h1>Your landing page</h1>
12+ <p>
13+ This file is <code>landing/index.html</code>. Replace it with your own, and put its
14+ css, images and scripts in this folder &mdash; relative paths work as normal.
15+ Or simple create a new one in pure html.
16+ </p>
17+ <a class="enter" href="/index.php?site=1">Enter the site</a>
18+ </main>
19+</body>
20+</html>
A layouts/.htaccess +17-0 View file
@@ -0,0 +1,17 @@
1+# Nothing inside a theme is meant to be requested directly: ZnoteX includes its
2+# shells and views from the page that owns them. Reaching one on its own means
3+# running it outside engine/init.php, with no $config and no session -
4+# unpredictable at best, and a way to read a theme's source at worst.
5+#
6+# The theme's own css, js, images and fonts still work: they do not match the
7+# list below.
8+
9+<FilesMatch "\.(php|phtml|php[0-9]|inc|sql|json|md|txt|ya?ml|ini|log|bak|dist|example)$">
10+ <IfModule mod_authz_core.c>
11+ Require all denied
12+ </IfModule>
13+ <IfModule !mod_authz_core.c>
14+ Order deny,allow
15+ Deny from all
16+ </IfModule>
17+</FilesMatch>
A layouts/default/aside.php +64-0 View file
@@ -0,0 +1,64 @@
1+<?php
2+/**
3+ * Right column of the default theme.
4+ *
5+ * Included only because shells/default.php calls theme_sidebar(). A theme with
6+ * no sidebar simply does not call it, and does not need this file at all.
7+ *
8+ * Each widget() call loads widgets/<name>.php from this theme, falling back to
9+ * the default theme's copy.
10+ */
11+?>
12+<!-- RIGHT PANE -->
13+<div class="pull-right rightPane">
14+<?php
15+
16+if (user_logged_in() === true) {
17+
18+ widget('myaccount');
19+
20+ if (isset($user_data) && has_admin_panel_access($user_data)) {
21+ widget('admin');
22+ }
23+
24+} else {
25+ widget('login');
26+}
27+
28+if (!empty($config['otservers_eu_voting']['enabled'])) {
29+ widget('vote');
30+}
31+
32+widget('charactersearch');
33+widget('topplayers');
34+widget('highscore');
35+
36+if (!empty($config['powergamers']['enabled'])) {
37+ widget('powergamers');
38+}
39+
40+widget('serverinfo');
41+
42+if (!empty($config['ServerEngine']) && znote_server_adapter()->normalizedEngine() !== 'TFS_02') {
43+ widget('houses');
44+}
45+
46+/* FOLLOW BLOCK */
47+if (!empty($follow['enabled'])): ?>
48+ <div class="well">
49+ <div class="header"><?= t('widget.follow.title') ?></div>
50+ <div class="body">
51+ <table class="smedia centralizeContent">
52+ <tr>
53+ <td><a href="<?= htmlspecialchars($follow['facebook'] ?? '#') ?>" target="_blank" rel="noopener"><i class="fa fa-facebook"></i></a></td>
54+ <td><a href="<?= htmlspecialchars($follow['twitter'] ?? '#') ?>" target="_blank" rel="noopener"><i class="fa fa-twitter"></i></a></td>
55+ <td><a href="<?= htmlspecialchars($follow['youtube'] ?? '#') ?>" target="_blank" rel="noopener"><i class="fa fa-youtube"></i></a></td>
56+ <td><a href="<?= htmlspecialchars($follow['twitch'] ?? '#') ?>" target="_blank" rel="noopener"><i class="fa fa-twitch"></i></a></td>
57+ </tr>
58+ </table>
59+ </div>
60+ </div>
61+<?php endif; ?>
62+
63+</div>
64+<!-- RIGHT PANE END -->
A layouts/default/assets/css/resp.css +29-0 View file
@@ -0,0 +1,29 @@
1+@media screen and (max-width:1300px){
2+ .main {
3+ width: 1100px;
4+ }
5+ .banner {
6+ height: 214px;
7+ }
8+}
9+
10+@media screen and (max-width:1200px){
11+ .main {
12+ width: 1000px;
13+ }
14+ .banner {
15+ height: 192px;
16+ }
17+}
18+
19+@media screen and (max-width:1100px){
20+ .main {
21+ width: 900px;
22+ }
23+ nav .container > div > ul > li > a {
24+ padding: 20px 10px;
25+ }
26+ .banner {
27+ height: 170px;
28+ }
29+}
A layouts/default/assets/img/atomio_front.jpg +0-0 View file
Binary file not shown.
A layouts/default/assets/img/atomio_profile.jpg +0-0 View file
Binary file not shown.
A layouts/default/assets/img/bg.png +0-0 View file
Binary file not shown.
A layouts/default/assets/img/guild_default.jpg +0-0 View file
Binary file not shown.
A layouts/default/assets/img/header.png +0-0 View file
Binary file not shown.
A layouts/default/assets/img/mainbg.jpg +0-0 View file
Binary file not shown.
A layouts/default/assets/img/modern.png +0-0 View file
Binary file not shown.
A layouts/default/assets/img/prev1.png +0-0 View file
Binary file not shown.
A layouts/default/assets/img/prev2.png +0-0 View file
Binary file not shown.
A layouts/default/assets/img/preview.png +0-0 View file
Binary file not shown.
A layouts/default/assets/img/sm_icons.png +0-0 View file
Binary file not shown.
A layouts/default/assets/js/countdown.js +50-0 View file
@@ -0,0 +1,50 @@
1+function countDown(elid, seconds, msg){
2+ // Set the date we're counting down to
3+ var countDownDate = new Date();
4+ countDownDate.setSeconds(countDownDate.getSeconds() + seconds);
5+ var countDownDate = countDownDate.getTime();
6+
7+ // Update the count down every 1 second
8+ window.countDownInterval = setInterval(function() {
9+
10+ // Get todays date and time
11+ var now = new Date().getTime();
12+
13+ // Find the distance between now and the count down date
14+ var distance = countDownDate - now;
15+
16+ // Time calculations for days, hours, minutes and seconds
17+ var days = Math.floor(distance / (1000 * 60 * 60 * 24));
18+ var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
19+ var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
20+ var seconds = Math.floor((distance % (1000 * 60)) / 1000);
21+
22+ // Display the result in the element with id="demo"
23+ document.getElementById(elid).innerHTML = "<b>Server starts in:</b> "+ days + "d " + hours + "h " + minutes + "m " + seconds + "s ";
24+
25+ // If the count down is finished, write some text
26+ if (distance < 0) {
27+ clearInterval(window.countDownInterval);
28+ document.getElementById(elid).innerHTML = msg;
29+ }
30+ }, 1000);
31+
32+ // Get todays date and time
33+ var now = new Date().getTime();
34+
35+ // Find the distance between now and the count down date
36+ var distance = countDownDate - now;
37+
38+ // Time calculations for days, hours, minutes and seconds
39+ var days = Math.floor(distance / (1000 * 60 * 60 * 24));
40+ var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
41+ var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
42+ var seconds = Math.floor((distance % (1000 * 60)) / 1000);
43+
44+ // Display the result in the element with id="demo"
45+ document.getElementById(elid).innerHTML = "<b>Server starts in:</b> "+ days + "d " + hours + "h " + minutes + "m " + seconds + "s ";
46+
47+ if (distance < 0) {
48+ document.getElementById(elid).innerHTML = msg;
49+ }
50+}
A layouts/default/layout_config.php +19-0 View file
@@ -0,0 +1,19 @@
1+<?php
2+ // Set enabled to false to remove the widget
3+ $follow = array(
4+ "enabled" => false,
5+ "twitter" => "https://www.twitter.com/",
6+ "facebook" => "https://www.facebook.com/",
7+ "youtube" => "https://www.youtube.com/",
8+ "twitch" => "https://www.twitch.tv/"
9+ );
10+
11+ // Use same date format when changing: yyyy-mm-dd hh:mm
12+ $countDown = "2020-06-10 01:00";
13+
14+ // Hide countdown after 1 day (24 hours) after countDown
15+ $countDown_hide = 1 * 24 * 60 * 60;
16+
17+ // Say this after countdown, and before the row is hidden
18+ $countDown_complete = "<span style='color: green;'>ONLINE</span>";
19+?>
Top