Browse Source

Empty commit

biotorrents 8 months ago
parent
commit
b4fb8c0c52
100 changed files with 30718 additions and 0 deletions
  1. 9
    0
      .gitignore
  2. 4
    0
      ajax.php
  3. 1
    0
      announce.php
  4. 5
    0
      artist.php
  5. 5
    0
      better.php
  6. 4
    0
      blog.php
  7. 5
    0
      bookmarks.php
  8. 4
    0
      browse.php
  9. 286
    0
      classes/artists.class.php
  10. 499
    0
      classes/autoenable.class.php
  11. 67
    0
      classes/autoload.php
  12. 163
    0
      classes/badges.class.php
  13. 111
    0
      classes/bencode.class.php
  14. 208
    0
      classes/bencodedecode.class.php
  15. 175
    0
      classes/bencodetorrent.class.php
  16. 39
    0
      classes/bitcoinrpc.class.php
  17. 110
    0
      classes/bookmarks.class.php
  18. 5
    0
      classes/browse.class.php
  19. 476
    0
      classes/cache.class.php
  20. 232
    0
      classes/charts.class.php
  21. 78
    0
      classes/collages.class.php
  22. 573
    0
      classes/comments.class.php
  23. 116
    0
      classes/commentsview.class.php
  24. 1316
    0
      classes/config.template
  25. 54
    0
      classes/cookie.class.php
  26. 41
    0
      classes/crypto.class.php
  27. 827
    0
      classes/debug.class.php
  28. 881
    0
      classes/donations.class.php
  29. 214
    0
      classes/donationsbitcoin.class.php
  30. 227
    0
      classes/donationsview.class.php
  31. 302
    0
      classes/env.class.php
  32. 114
    0
      classes/feed.class.php
  33. 699
    0
      classes/format.class.php
  34. 417
    0
      classes/forums.class.php
  35. 17
    0
      classes/g.class.php
  36. 67
    0
      classes/image.class.php
  37. 58
    0
      classes/imagetools.class.php
  38. 31
    0
      classes/inbox.class.php
  39. 270
    0
      classes/invite_tree.class.php
  40. 153
    0
      classes/ipv4.class.php
  41. 131
    0
      classes/json.class.php
  42. 60
    0
      classes/lockedaccounts.class.php
  43. 305
    0
      classes/loginwatch.class.php
  44. 89
    0
      classes/mass_user_bookmarks_editor.class.php
  45. 66
    0
      classes/mass_user_torrents_editor.class.php
  46. 300
    0
      classes/mass_user_torrents_table_view.class.php
  47. 580
    0
      classes/misc.class.php
  48. 656
    0
      classes/mysql.class.php
  49. 729
    0
      classes/notificationsmanager.class.php
  50. 140
    0
      classes/notificationsmanagerview.class.php
  51. 115
    0
      classes/paranoia.class.php
  52. 112
    0
      classes/permissions.class.php
  53. 278
    0
      classes/permissions_form.php
  54. 45
    0
      classes/proxies.class.php
  55. 30
    0
      classes/reports.class.php
  56. 264
    0
      classes/requests.class.php
  57. 30
    0
      classes/revisionhistory.class.php
  58. 46
    0
      classes/revisionhistoryview.class.php
  59. 572
    0
      classes/script_start.php
  60. 86
    0
      classes/security.class.php
  61. 304
    0
      classes/sitehistory.class.php
  62. 236
    0
      classes/sitehistoryview.class.php
  63. 16
    0
      classes/slaves.class.php
  64. 162
    0
      classes/sphinxql.class.php
  65. 410
    0
      classes/sphinxqlquery.class.php
  66. 156
    0
      classes/sphinxqlresult.class.php
  67. 422
    0
      classes/subscriptions.class.php
  68. 323
    0
      classes/tags.class.php
  69. 83
    0
      classes/templates.class.php
  70. 169
    0
      classes/testing.class.php
  71. 182
    0
      classes/testingview.class.php
  72. 1197
    0
      classes/text.class.php
  73. 244
    0
      classes/textarea_preview.class.php
  74. 183
    0
      classes/time.class.php
  75. 341
    0
      classes/tools.class.php
  76. 91
    0
      classes/top10view.class.php
  77. 348
    0
      classes/torrent.class.php
  78. 1494
    0
      classes/torrent_form.class.NEW.2020-12-10.php
  79. 1478
    0
      classes/torrent_form.class.php
  80. 1288
    0
      classes/torrents.class.php
  81. 261
    0
      classes/torrentsdl.class.php
  82. 737
    0
      classes/torrentsearch.class.php
  83. 195
    0
      classes/tracker.class.php
  84. 138
    0
      classes/twofa.class.php
  85. 576
    0
      classes/u2f.class.php
  86. 168
    0
      classes/useragent.class.php
  87. 173
    0
      classes/userrank.class.php
  88. 1016
    0
      classes/users.class.php
  89. 653
    0
      classes/util.php
  90. 566
    0
      classes/validate.class.php
  91. 1994
    0
      classes/vendor/Parsedown.php
  92. 686
    0
      classes/vendor/ParsedownExtra.php
  93. 410
    0
      classes/vendor/TwitterAPIExchange.php
  94. 216
    0
      classes/view.class.php
  95. 111
    0
      classes/wiki.class.php
  96. 178
    0
      classes/zip.class.php
  97. 6
    0
      collage.php
  98. 5
    0
      collages.php
  99. 5
    0
      comments.php
  100. 0
    0
      contest.php

+ 9
- 0
.gitignore View File

@@ -0,0 +1,9 @@
1
+classes/config.php
2
+_packages/**/.git/**
3
+
4
+static/styles/*.css
5
+static/styles/assets/fonts/**
6
+static/common/badges/**
7
+
8
+.DS_Store
9
+*.sw*

+ 4
- 0
ajax.php View File

@@ -0,0 +1,4 @@
1
+<?php
2
+declare(strict_types=1);
3
+
4
+require_once 'classes/script_start.php';

+ 1
- 0
announce.php View File

@@ -0,0 +1 @@
1
+d14:failure reason40:Invalid .torrent, try downloading again.e

+ 5
- 0
artist.php View File

@@ -0,0 +1,5 @@
1
+<?php
2
+declare(strict_types=1);
3
+
4
+define('ERROR_EXCEPTION', true);
5
+require_once 'classes/script_start.php';

+ 5
- 0
better.php View File

@@ -0,0 +1,5 @@
1
+<?php
2
+declare(strict_types=1);
3
+
4
+define('ERROR_EXCEPTION', true);
5
+require_once 'classes/script_start.php';

+ 4
- 0
blog.php View File

@@ -0,0 +1,4 @@
1
+<?php
2
+declare(strict_types=1);
3
+
4
+require_once 'classes/script_start.php';

+ 5
- 0
bookmarks.php View File

@@ -0,0 +1,5 @@
1
+<?php
2
+declare(strict_types=1);
3
+
4
+define('ERROR_EXCEPTION', true);
5
+require_once 'classes/script_start.php';

+ 4
- 0
browse.php View File

@@ -0,0 +1,4 @@
1
+<?php
2
+declare(strict_types=1);
3
+
4
+header('Location: torrents.php');

+ 286
- 0
classes/artists.class.php View File

@@ -0,0 +1,286 @@
1
+<?php
2
+
3
+class Artists
4
+{
5
+
6
+  /**
7
+   * Given an array of GroupIDs, return their associated artists.
8
+   *
9
+   * @param array $GroupIDs
10
+   * @return an array of the following form:
11
+   *  GroupID => {
12
+   *    [ArtistType] => {
13
+   *      id, name, aliasid
14
+   *    }
15
+   *  }
16
+   *
17
+   * ArtistType is an int. It can be:
18
+   * 1 => Main artist
19
+   * 2 => Guest artist
20
+   * 4 => Composer
21
+   * 5 => Conductor
22
+   * 6 => DJ
23
+   */
24
+    public static function get_artists($GroupIDs)
25
+    {
26
+        $Results = [];
27
+        $DBs = [];
28
+
29
+        foreach ($GroupIDs as $GroupID) {
30
+            if (!is_number($GroupID)) {
31
+                continue;
32
+            }
33
+
34
+            $Artists = G::$Cache->get_value('groups_artists_'.$GroupID);
35
+            if (is_array($Artists)) {
36
+                $Results[$GroupID] = $Artists;
37
+            } else {
38
+                $DBs[] = $GroupID;
39
+            }
40
+        }
41
+
42
+        if (count($DBs) > 0) {
43
+            $IDs = implode(',', $DBs);
44
+            if (empty($IDs)) {
45
+                $IDs = 'null';
46
+            }
47
+
48
+            $QueryID = G::$DB->get_query_id();
49
+            G::$DB->query("
50
+            SELECT
51
+              ta.`GroupID`,
52
+              ta.`ArtistID`,
53
+              ag.`Name`
54
+            FROM
55
+              `torrents_artists` AS ta
56
+            JOIN `artists_group` AS ag
57
+            ON
58
+              ta.`ArtistID` = ag.`ArtistID`
59
+            WHERE
60
+              ta.`GroupID` IN($IDs)
61
+            ORDER BY
62
+              ta.`GroupID` ASC,
63
+              ag.`Name` ASC;
64
+            ");
65
+
66
+            while (list($GroupID, $ArtistID, $ArtistName) = G::$DB->next_record(MYSQLI_BOTH, false)) {
67
+                $Results[$GroupID][] = array('id' => $ArtistID, 'name' => $ArtistName);
68
+                $New[$GroupID][] = array('id' => $ArtistID, 'name' => $ArtistName);
69
+            }
70
+
71
+            G::$DB->set_query_id($QueryID);
72
+            foreach ($DBs as $GroupID) {
73
+                if (isset($New[$GroupID])) {
74
+                    G::$Cache->cache_value("groups_artists_$GroupID", $New[$GroupID]);
75
+                } else {
76
+                    G::$Cache->cache_value("groups_artists_$GroupID", []);
77
+                }
78
+            }
79
+
80
+            $Missing = array_diff($GroupIDs, array_keys($Results));
81
+            if (!empty($Missing)) {
82
+                $Results += array_fill_keys($Missing, []);
83
+            }
84
+        }
85
+        return $Results;
86
+    }
87
+
88
+    /**
89
+     * Convenience function for get_artists, when you just need one group.
90
+     *
91
+     * @param int $GroupID
92
+     * @return array - see get_artists
93
+     */
94
+    public static function get_artist($GroupID)
95
+    {
96
+        $Results = Artists::get_artists(array($GroupID));
97
+        return $Results[$GroupID];
98
+    }
99
+
100
+    /**
101
+     * Format an array of artists for display.
102
+     * todo: Revisit the logic of this, see if we can helper-function the copypasta.
103
+     *
104
+     * @param array Artists an array of the form output by get_artists
105
+     * @param boolean $MakeLink if true, the artists will be links, if false, they will be text.
106
+     * @param boolean $IncludeHyphen if true, appends " - " to the end.
107
+     * @param $Escape if true, output will be escaped. Think carefully before setting it false.
108
+     */
109
+    public static function display_artists($Artists, $MakeLink = true, $IncludeHyphen = true, $Escape = true)
110
+    {
111
+        if (!empty($Artists)) {
112
+            $ampersand = ($Escape) ? ' &amp; ' : ' & ';
113
+            $link = ($IncludeHyphen? '🧑&nbsp;' : '');
114
+    
115
+            switch (count($Artists)) {
116
+            case 0:
117
+                break;
118
+
119
+            case 3:
120
+                $link .= Artists::display_artist($Artists[2], $MakeLink, $Escape). ", ";
121
+                // no break
122
+
123
+            case 2:
124
+                $link .= Artists::display_artist($Artists[1], $MakeLink, $Escape). ", ";
125
+                // no break
126
+
127
+            case 1:
128
+                $link .= Artists::display_artist($Artists[0], $MakeLink, $Escape);
129
+                break;
130
+
131
+            default:
132
+                $link = ($IncludeHyphen?'🧑&nbsp;':'').Artists::display_artist($Artists[0], $MakeLink, $Escape).' et al.';
133
+                #$link = 'Various'.($IncludeHyphen?' &ndash; ':'');
134
+        }
135
+            return $link;
136
+        } else {
137
+            return '';
138
+        }
139
+    }
140
+    
141
+    /**
142
+     * Formats a single artist name.
143
+     *
144
+     * @param array $Artist an array of the form ('id' => ID, 'name' => Name)
145
+     * @param boolean $MakeLink If true, links to the artist page.
146
+     * @param boolean $Escape If false and $MakeLink is false, returns the unescaped, unadorned artist name.
147
+     * @return string Formatted artist name.
148
+     */
149
+    public static function display_artist($Artist, $MakeLink = true, $Escape = true)
150
+    {
151
+        if ($MakeLink && !$Escape) {
152
+            error('Invalid parameters to Artists::display_artist()');
153
+        } elseif ($MakeLink) {
154
+            return '<a href="artist.php?id='.$Artist['id'].'">'.display_str($Artist['name']).'</a>';
155
+        } elseif ($Escape) {
156
+            return display_str($Artist['name']);
157
+        } else {
158
+            return $Artist['name'];
159
+        }
160
+    }
161
+
162
+    /**
163
+     * Deletes an artist and their requests, wiki, and tags.
164
+     * Does NOT delete their torrents.
165
+     *
166
+     * @param int $ArtistID
167
+     */
168
+    public static function delete_artist($ArtistID)
169
+    {
170
+        $QueryID = G::$DB->get_query_id();
171
+        G::$DB->query("
172
+        SELECT
173
+          `NAME`
174
+        FROM
175
+          `artists_group`
176
+        WHERE
177
+          `ArtistID` = $ArtistID
178
+        ");
179
+        list($Name) = G::$DB->next_record(MYSQLI_NUM, false);
180
+
181
+        // Delete requests
182
+        G::$DB->query("
183
+        SELECT
184
+          `RequestID`
185
+        FROM
186
+          `requests_artists`
187
+        WHERE
188
+          `ArtistID` = $ArtistID AND `ArtistID` != 0
189
+        ");
190
+
191
+        $Requests = G::$DB->to_array();
192
+        foreach ($Requests as $Request) {
193
+            list($RequestID) = $Request;
194
+            G::$DB->query("
195
+            DELETE
196
+            FROM
197
+              `requests`
198
+            WHERE
199
+              `ID` = $RequestID
200
+            ");
201
+
202
+            G::$DB->query("
203
+            DELETE
204
+            FROM
205
+              `requests_votes`
206
+            WHERE
207
+              `RequestID` = $RequestID
208
+            ");
209
+
210
+            G::$DB->query("
211
+            DELETE
212
+            FROM
213
+              `requests_tags`
214
+            WHERE
215
+              `RequestID` = $RequestID
216
+            ");
217
+
218
+            G::$DB->query("
219
+            DELETE
220
+            FROM
221
+              `requests_artists`
222
+            WHERE
223
+              `RequestID` = $RequestID
224
+            ");
225
+        }
226
+
227
+        // Delete artist
228
+        G::$DB->query("
229
+        DELETE
230
+        FROM
231
+          `artists_group`
232
+        WHERE
233
+          `ArtistID` = $ArtistID
234
+        ");
235
+        G::$Cache->decrement('stats_artist_count');
236
+
237
+        // Delete wiki revisions
238
+        G::$DB->query("
239
+        DELETE
240
+        FROM
241
+          `wiki_artists`
242
+        WHERE
243
+          `PageID` = $ArtistID
244
+        ");
245
+
246
+        // Delete tags
247
+        G::$DB->query("
248
+        DELETE
249
+        FROM
250
+          `artists_tags`
251
+        WHERE
252
+          `ArtistID` = $ ArtistID
253
+        ");
254
+
255
+        // Delete artist comments, subscriptions and quote notifications
256
+        Comments::delete_page('artist', $ArtistID);
257
+        G::$Cache->delete_value("artist_$ArtistID");
258
+        G::$Cache->delete_value("artist_groups_$ArtistID");
259
+
260
+        // Record in log
261
+        if (!empty(G::$LoggedUser['Username'])) {
262
+            $Username = G::$LoggedUser['Username'];
263
+        } else {
264
+            $Username = 'System';
265
+        }
266
+        
267
+        Misc::write_log("Artist $ArtistID ($Name) was deleted by $Username");
268
+        G::$DB->set_query_id($QueryID);
269
+    }
270
+
271
+    /**
272
+     * Remove LRM (left-right-marker) and trims, because people copypaste carelessly.
273
+     * If we don't do this, we get seemingly duplicate artist names.
274
+     * todo: make stricter, e.g., on all whitespace characters or Unicode normalisation
275
+     *
276
+     * @param string $ArtistName
277
+     */
278
+    public static function normalise_artist_name($ArtistName)
279
+    {
280
+        // \u200e is &lrm;
281
+        $ArtistName = trim($ArtistName);
282
+        $ArtistName = preg_replace('/^(\xE2\x80\x8E)+/', '', $ArtistName);
283
+        $ArtistName = preg_replace('/(\xE2\x80\x8E)+$/', '', $ArtistName);
284
+        return trim(preg_replace('/ +/', ' ', $ArtistName));
285
+    }
286
+}

+ 499
- 0
classes/autoenable.class.php View File

@@ -0,0 +1,499 @@
1
+<?php
2
+
3
+# todo: Check strict equality gently
4
+class AutoEnable
5
+{
6
+    // Constants for database values
7
+    const APPROVED = 1;
8
+    const DENIED = 2;
9
+    const DISCARDED = 3;
10
+
11
+    // Cache key to store the number of enable requests
12
+    const CACHE_KEY_NAME = 'num_enable_requests';
13
+
14
+    // The default request rejected message
15
+    const REJECTED_MESSAGE = <<<EOT
16
+    <p>
17
+      Your request to re-enable your account has been rejected.
18
+    </p>
19
+
20
+    <p>
21
+      This may be because a request is already pending for your username,
22
+      or because a recent request was denied.
23
+    </p>
24
+EOT;
25
+
26
+    // The default request received message
27
+    const RECEIVED_MESSAGE = <<<EOT
28
+    <p>
29
+      Your request to re-enable your account has been received.
30
+    </p>
31
+    
32
+    <p>
33
+      Most requests are responded to within minutes.
34
+      Remember to check your spam folder.
35
+    </p>
36
+EOT;
37
+
38
+    /**
39
+     * Handle a new enable request
40
+     *
41
+     * @param string $Username The user's username
42
+     * @param string $Email The user's email address
43
+     * @return string The output
44
+     */
45
+    public static function new_request($Username, $Email)
46
+    {
47
+        if (empty($Username)) {
48
+            header('Location: login.php');
49
+            error();
50
+        }
51
+
52
+        // Get the user's ID
53
+        G::$DB->query("
54
+        SELECT
55
+          um.`ID`,
56
+          ui.`BanReason`
57
+        FROM
58
+          `users_main` AS um
59
+        JOIN `users_info` ui ON
60
+          ui.`UserID` = um.`ID`
61
+        WHERE
62
+          um.`Username` = '$Username'
63
+          AND um.`Enabled` = '2'
64
+        ");
65
+
66
+        if (G::$DB->has_results()) {
67
+            // Make sure the user can make another request
68
+            list($UserID, $BanReason) = G::$DB->next_record();
69
+            G::$DB->query("
70
+            SELECT
71
+              1
72
+            FROM
73
+              `users_enable_requests`
74
+            WHERE
75
+              `UserID` = '$UserID' AND(
76
+                (
77
+                  `Timestamp` > NOW() - INTERVAL 1 WEEK
78
+                  AND `HandledTimestamp` IS NULL)
79
+                  OR(
80
+                    `Timestamp` > NOW() - INTERVAL 2 MONTH
81
+                    AND Outcome = '".self::DENIED."')
82
+                )
83
+            ");
84
+        }
85
+
86
+        $IP = $_SERVER['REMOTE_ADDR'];
87
+        if (G::$DB->has_results() || !isset($UserID)) {
88
+            // User already has/had a pending activation request or username is invalid
89
+            $Output = sprintf(self::REJECTED_MESSAGE, DISABLED_CHAN, BOT_SERVER);
90
+            if (isset($UserID)) {
91
+                Tools::update_user_notes(
92
+                    $UserID,
93
+                    sqltime() . " - Enable request rejected from $IP\n\n"
94
+                );
95
+            }
96
+        } else {
97
+            // New disable activation request
98
+            $UserAgent = db_string($_SERVER['HTTP_USER_AGENT']);
99
+            G::$DB->query(
100
+                "
101
+                INSERT INTO `users_enable_requests`(
102
+                  `UserID`,
103
+                  `Email`,
104
+                  `IP`,
105
+                  `UserAgent`,
106
+                  `Timestamp`
107
+                )
108
+                VALUES(?, ?, ?, ?, NOW())",
109
+                $UserID,
110
+                Crypto::encrypt($Email),
111
+                Crypto::encrypt($IP),
112
+                $UserAgent
113
+            );
114
+            $RequestID = G::$DB->inserted_id();
115
+
116
+            // Cache the number of requests for the modbar
117
+            G::$Cache->increment_value(self::CACHE_KEY_NAME);
118
+            setcookie('username', '', time() - 60 * 60, '/', '', false);
119
+            $Output = self::RECEIVED_MESSAGE;
120
+
121
+            Tools::update_user_notes(
122
+                $UserID,
123
+                sqltime() . " - Enable request " . G::$DB->inserted_id() . " received from $IP\n\n"
124
+            );
125
+
126
+            if ($BanReason === 3) {
127
+                self::handle_requests(
128
+                    [$RequestID],
129
+                    self::APPROVED,
130
+                    'Automatically approved (inactivity)'
131
+                );
132
+            }
133
+        }
134
+        return $Output;
135
+    }
136
+
137
+    /*
138
+     * Handle requests
139
+     *
140
+     * @param int|int[] $IDs An array of IDs, or a single ID
141
+     * @param int $Status The status to mark the requests as
142
+     * @param string $Comment The staff member comment
143
+     */
144
+    public static function handle_requests($IDs, $Status, $Comment)
145
+    {
146
+        # Error checking
147
+        if ($Status !== self::APPROVED && $Status !== self::DENIED && $Status !== self::DISCARDED) {
148
+            error(404);
149
+        }
150
+
151
+        $ENV = ENV::go();
152
+        $UserInfo = [];
153
+        $IDs = (!is_array($IDs)) ? [$IDs] : $IDs;
154
+
155
+        if (count($IDs) === 0) {
156
+            error(404);
157
+        }
158
+
159
+        foreach ($IDs as $ID) {
160
+            if (!is_number($ID)) {
161
+                error(404);
162
+            }
163
+        }
164
+
165
+        G::$DB->query("
166
+        SELECT
167
+          `Email`,
168
+          `ID`,
169
+          `UserID`
170
+        FROM
171
+          `users_enable_requests`
172
+        WHERE
173
+          `ID` IN(".implode(',', $IDs).")
174
+          AND `Outcome` IS NULL
175
+        ");
176
+        $Results = G::$DB->to_array(false, MYSQLI_NUM);
177
+
178
+        if ($Status !== self::DISCARDED) {
179
+            // Prepare email
180
+            require_once SERVER_ROOT.'/classes/templates.class.php';
181
+            $TPL = new TEMPLATE;
182
+
183
+            if ($Status === self::APPROVED) {
184
+                $TPL->open(SERVER_ROOT.'/templates/enable_request_accepted.tpl');
185
+                $TPL->set('SITE_DOMAIN', SITE_DOMAIN);
186
+            } else {
187
+                $TPL->open(SERVER_ROOT.'/templates/enable_request_denied.tpl');
188
+            }
189
+            $TPL->set('SITE_NAME', $ENV->SITE_NAME);
190
+
191
+            foreach ($Results as $Result) {
192
+                list($Email, $ID, $UserID) = $Result;
193
+                $Email = Crypto::decrypt($Email);
194
+                $UserInfo[] = array($ID, $UserID);
195
+
196
+                if ($Status === self::APPROVED) {
197
+                    // Generate token
198
+                    $Token = db_string(Users::make_secret());
199
+                    G::$DB->query("
200
+                    UPDATE
201
+                      `users_enable_requests`
202
+                    SET
203
+                      `Token` = ?
204
+                    WHERE
205
+                      `ID` = ?,
206
+                      $Token,
207
+                      $ID
208
+                    ");
209
+                    $TPL->set('TOKEN', $Token);
210
+                }
211
+
212
+                // Send email
213
+                $Subject = "Your enable request for $ENV->SITE_NAME has been ";
214
+                $Subject .= ($Status === self::APPROVED) ? 'approved' : 'denied';
215
+                Misc::send_email($Email, $Subject, $TPL->get(), 'noreply');
216
+            }
217
+        } else {
218
+            foreach ($Results as $Result) {
219
+                list(, $ID, $UserID) = $Result;
220
+                $UserInfo[] = array($ID, $UserID);
221
+            }
222
+        }
223
+
224
+        // User notes stuff
225
+        $StaffID = G::$LoggedUser['ID'] ?? 0;
226
+        G::$DB->query("
227
+        SELECT
228
+          `Username`
229
+        FROM
230
+          `users_main`
231
+        WHERE
232
+          `ID` = ?,
233
+          $StaffID
234
+        ");
235
+
236
+        if (G::$DB->has_results()) {
237
+            list($StaffUser) = G::$DB->next_record();
238
+        } else {
239
+            $StaffUser = "System";
240
+            $StaffID = 0;
241
+        }
242
+
243
+        foreach ($UserInfo as $User) {
244
+            list($ID, $UserID) = $User;
245
+            $BaseComment = sqltime()." - Enable request $ID ".strtolower(self::get_outcome_string($Status)).' by [user]'.$StaffUser.'[/user]';
246
+            $BaseComment .= (!empty($Comment)) ? "\nReason: $Comment\n\n" : "\n\n";
247
+            Tools::update_user_notes($UserID, $BaseComment);
248
+        }
249
+
250
+        // Update database values and decrement cache
251
+        G::$DB->query("
252
+        UPDATE
253
+          `users_enable_requests`
254
+        SET
255
+          `HandledTimestamp` = NOW(), `CheckedBy` = ?, `Outcome` = ?
256
+        WHERE
257
+          `ID` IN(".implode(',', $IDs)."),
258
+          $StaffID,
259
+          $Status
260
+        ");
261
+        G::$Cache->decrement_value(self::CACHE_KEY_NAME, count($IDs));
262
+    }
263
+
264
+    /**
265
+     * Unresolve a discarded request
266
+     *
267
+     * @param int $ID The request ID
268
+     */
269
+    public static function unresolve_request($ID)
270
+    {
271
+        $ID = (int) $ID;
272
+        if (empty($ID)) {
273
+            error(404);
274
+        }
275
+
276
+        G::$DB->query("
277
+        SELECT
278
+          `UserID`
279
+        FROM
280
+          `users_enable_requests`
281
+        WHERE
282
+          `Outcome` = '" . self::DISCARDED . "' AND `ID` = '$ID'
283
+        ");
284
+
285
+        if (!G::$DB->has_results()) {
286
+            error(404);
287
+        } else {
288
+            list($UserID) = G::$DB->next_record();
289
+        }
290
+
291
+        G::$DB->query("
292
+        SELECT
293
+          `Username`
294
+        FROM
295
+          `users_main`
296
+        WHERE
297
+          `ID` = '".G::$LoggedUser['ID']."'
298
+        ");
299
+        list($StaffUser) = G::$DB->next_record();
300
+
301
+        Tools::update_user_notes($UserID, sqltime()." - Enable request $ID unresolved by [user]".$StaffUser.'[/user]'."\n\n");
302
+        G::$DB->query("
303
+        UPDATE
304
+          `users_enable_requests`
305
+        SET
306
+          `Outcome` = NULL,
307
+          `HandledTimestamp` = NULL,
308
+          `CheckedBy` = NULL
309
+        WHERE
310
+          `ID` = '$ID'
311
+        ");
312
+        G::$Cache->increment_value(self::CACHE_KEY_NAME);
313
+    }
314
+
315
+    /**
316
+     * Get the corresponding outcome string for a numerical value
317
+     *
318
+     * @param int $Outcome The outcome integer
319
+     * @return string The formatted output string
320
+     */
321
+    public static function get_outcome_string($Outcome)
322
+    {
323
+        if ($Outcome === self::APPROVED) {
324
+            $String = 'Approved';
325
+        } elseif ($Outcome === self::DENIED) {
326
+            $String = 'Rejected';
327
+        } elseif ($Outcome === self::DISCARDED) {
328
+            $String = 'Discarded';
329
+        } else {
330
+            $String = '---';
331
+        }
332
+        return $String;
333
+    }
334
+
335
+    /**
336
+     * Handle a user's request to enable an account
337
+     *
338
+     * @param string $Token The token
339
+     * @return string The error output, or an empty string
340
+     */
341
+    public static function handle_token($Token)
342
+    {
343
+        $Token = db_string($Token);
344
+        G::$DB->query("
345
+        SELECT
346
+          uer.`UserID`,
347
+          uer.`HandledTimestamp`,
348
+          um.`torrent_pass`,
349
+          um.`Visible`,
350
+          um.`IP`
351
+        FROM
352
+          `users_enable_requests` AS uer
353
+        LEFT JOIN `users_main` AS um
354
+        ON
355
+          uer.`UserID` = um.`ID`
356
+        WHERE
357
+          `Token` = '$Token'
358
+        ");
359
+
360
+        if (G::$DB->has_results()) {
361
+            list($UserID, $Timestamp, $TorrentPass, $Visible, $IP) = G::$DB->next_record();
362
+            G::$DB->query("
363
+            UPDATE
364
+              `users_enable_requests`
365
+            SET
366
+              `Token` = NULL
367
+            WHERE
368
+              `Token` = '$Token'
369
+            ");
370
+
371
+            if ($Timestamp < time_minus(3600 * 48)) {
372
+                // Old request
373
+                Tools::update_user_notes($UserID, sqltime()." - Tried to use an expired enable token from ".$_SERVER['REMOTE_ADDR']."\n\n");
374
+                $Err = "Token has expired. Please visit ".DISABLED_CHAN." on ".BOT_SERVER." to discuss this with staff.";
375
+            } else {
376
+                // Good request, decrement cache value and enable account
377
+                G::$Cache->decrement_value(AutoEnable::CACHE_KEY_NAME);
378
+                $VisibleTrIP = ($Visible && Crypto::decrypt($IP) !== '127.0.0.1') ? '1' : '0';
379
+                Tracker::update_tracker('add_user', array('id' => $UserID, 'passkey' => $TorrentPass, 'visible' => $VisibleTrIP));
380
+
381
+                G::$DB->query("
382
+                UPDATE
383
+                  `users_main`
384
+                SET
385
+                  `Enabled` = '1',
386
+                  `can_leech` = '1'
387
+                WHERE
388
+                  `ID` = '$UserID'
389
+                ");
390
+                
391
+                G::$DB->query("
392
+                UPDATE
393
+                  `users_info`
394
+                SET
395
+                  `BanReason` = '0'
396
+                WHERE
397
+                  `UserID` = '$UserID'
398
+                ");
399
+
400
+                G::$Cache->delete_value("user_info_$UserID");
401
+                $Err = "Your account has been enabled. You may now log in.";
402
+            }
403
+        } else {
404
+            $Err = "Invalid token.";
405
+        }
406
+        return $Err;
407
+    }
408
+
409
+    /**
410
+     * Build the search query, from the searchbox inputs
411
+     *
412
+     * @param int $UserID The user ID
413
+     * @param string $IP The IP
414
+     * @param string $SubmittedTimestamp The timestamp representing when the request was submitted
415
+     * @param int $HandledUserID The ID of the user that handled the request
416
+     * @param string $HandledTimestamp The timestamp representing when the request was handled
417
+     * @param int $OutcomeSearch The outcome of the request
418
+     * @param boolean $Checked Should checked requests be included?
419
+     * @return array The WHERE conditions for the query
420
+     */
421
+    public static function build_search_query($Username, $IP, $SubmittedBetween, $SubmittedTimestamp1, $SubmittedTimestamp2, $HandledUsername, $HandledBetween, $HandledTimestamp1, $HandledTimestamp2, $OutcomeSearch, $Checked)
422
+    {
423
+        $Where = [];
424
+
425
+        if (!empty($Username)) {
426
+            $Where[] = "um1.`Username` = '$Username'";
427
+        }
428
+
429
+        if (!empty($IP)) {
430
+            // todo: Make this work with encrypted IPs
431
+            $Where[] = "uer.`IP` = '$IP'";
432
+        }
433
+
434
+        if (!empty($SubmittedTimestamp1)) {
435
+            switch ($SubmittedBetween) {
436
+                case 'on':
437
+                    $Where[] = "DATE(uer.`Timestamp`) = DATE('$SubmittedTimestamp1')";
438
+                    break;
439
+
440
+                case 'before':
441
+                    $Where[] = "DATE(uer.`Timestamp`) < DATE('$SubmittedTimestamp1')";
442
+                    break;
443
+
444
+                case 'after':
445
+                    $Where[] = "DATE(uer.`Timestamp`) > DATE('$SubmittedTimestamp1')";
446
+                    break;
447
+
448
+                case 'between':
449
+                    if (!empty($SubmittedTimestamp2)) {
450
+                        $Where[] = "DATE(uer.`Timestamp`) BETWEEN DATE('$SubmittedTimestamp1') AND DATE('$SubmittedTimestamp2')";
451
+                    }
452
+                    break;
453
+
454
+                default:
455
+                    break;
456
+            }
457
+        }
458
+
459
+        if (!empty($HandledTimestamp1)) {
460
+            switch ($HandledBetween) {
461
+                case 'on':
462
+                    $Where[] = "DATE(uer.`HandledTimestamp`) = DATE('$HandledTimestamp1')";
463
+                    break;
464
+
465
+                case 'before':
466
+                    $Where[] = "DATE(uer.`HandledTimestamp`) < DATE('$HandledTimestamp1')";
467
+                    break;
468
+
469
+                case 'after':
470
+                    $Where[] = "DATE(uer.`HandledTimestamp`) > DATE('$HandledTimestamp1')";
471
+                    break;
472
+
473
+                case 'between':
474
+                    if (!empty($HandledTimestamp2)) {
475
+                        $Where[] = "DATE(uer.`HandledTimestamp`) BETWEEN DATE('$HandledTimestamp1') AND DATE('$HandledTimestamp2')";
476
+                    }
477
+                    break;
478
+
479
+                default:
480
+                    break;
481
+            }
482
+        }
483
+
484
+        if (!empty($HandledUsername)) {
485
+            $Where[] = "um2.`Username` = '$HandledUsername'";
486
+        }
487
+
488
+        if (!empty($OutcomeSearch)) {
489
+            $Where[] = "uer.`Outcome` = '$OutcomeSearch'";
490
+        }
491
+
492
+        if ($Checked) {
493
+            // This is to skip the if statement in enable_requests.php
494
+            $Where[] = "(uer.`Outcome` IS NULL OR uer.`Outcome` IS NOT NULL)";
495
+        }
496
+
497
+        return $Where;
498
+    }
499
+}

+ 67
- 0
classes/autoload.php View File

@@ -0,0 +1,67 @@
1
+<?php
2
+declare(strict_types=1);
3
+
4
+/**
5
+ * Autoload
6
+ *
7
+ * Load classes automatically when they're needed.
8
+ * The Gazelle convention is classes/lowercase_name.class.php.
9
+ *
10
+ * @param string $ClassName class name
11
+ * @see https://www.php.net/manual/en/language.oop5.autoload.php
12
+ */
13
+spl_autoload_register(function ($ClassName) {
14
+    $FilePath = SERVER_ROOT . '/classes/' . strtolower($ClassName) . '.class.php';
15
+    #$FilePath = $_SERVER['DOCUMENT_ROOT'] . '/classes/' . strtolower($ClassName) . '.class.php';
16
+
17
+    if (!file_exists($FilePath)) {
18
+        // todo: Rename the following classes to conform with the code guidelines
19
+        switch ($ClassName) {
20
+        case 'MASS_USER_BOOKMARKS_EDITOR':
21
+          $FileName = 'mass_user_bookmarks_editor.class';
22
+          break;
23
+
24
+        case 'MASS_USER_TORRENTS_EDITOR':
25
+          $FileName = 'mass_user_torrents_editor.class';
26
+          break;
27
+
28
+        case 'MASS_USER_TORRENTS_TABLE_VIEW':
29
+          $FileName = 'mass_user_torrents_table_view.class';
30
+          break;
31
+
32
+        case 'TEXTAREA_PREVIEW':
33
+          $FileName = 'textarea_preview.class';
34
+          break;
35
+
36
+        case 'TORRENT':
37
+        case 'BENCODE_DICT':
38
+        case 'BENCODE_LIST':
39
+          $FileName = 'torrent.class';
40
+          break;
41
+
42
+        case 'RecursiveArrayObject':
43
+          $FileName = 'env.class';
44
+          break;
45
+
46
+        case 'Parsedown':
47
+          $FileName = 'vendor/Parsedown';
48
+          break;
49
+
50
+        case 'ParsedownExtra':
51
+          $FileName = 'vendor/ParsedownExtra';
52
+          break;
53
+
54
+        case 'TwitterAPIExchange':
55
+          $FileName = 'vendor/TwitterAPIExchange';
56
+          break;
57
+
58
+        default:
59
+          error("Couldn't import class $ClassName");
60
+    }
61
+
62
+        $FilePath = SERVER_ROOT . "/classes/$FileName.php";
63
+        #$FilePath = $_SERVER['DOCUMENT_ROOT'] . "/classes/$FileName.php";
64
+    }
65
+
66
+    require_once $FilePath;
67
+});

+ 163
- 0
classes/badges.class.php View File

@@ -0,0 +1,163 @@
1
+<?php
2
+declare(strict_types = 1);
3
+
4
+class Badges
5
+{
6
+    /**
7
+     * Given a UserID, returns that user's badges
8
+     *
9
+     * @param int $UserID
10
+     * @return array of BadgeIDs
11
+     */
12
+    public static function get_badges($UserID)
13
+    {
14
+        return Users::user_info($UserID)['Badges'];
15
+    }
16
+
17
+
18
+    /**
19
+     * Awards UserID the given BadgeID
20
+     *
21
+     * @param int $UserID
22
+     * @param int $BadgeID
23
+     * @return bool success?
24
+     */
25
+    public static function award_badge($UserID, $BadgeID)
26
+    {
27
+        if (self::has_badge($UserID, $BadgeID)) {
28
+            return false;
29
+        } else {
30
+            $QueryID = G::$DB->get_query_id();
31
+            G::$DB->query("
32
+            INSERT INTO `users_badges`(`UserID`, `BadgeID`)
33
+            VALUES($UserID, $BadgeID)
34
+            ");
35
+
36
+            G::$DB->set_query_id($QueryID);
37
+            G::$Cache->delete_value("user_info_$UserID");
38
+            return true;
39
+        }
40
+    }
41
+
42
+
43
+    /**
44
+     * Given a UserID, return that user's displayed badges
45
+     *
46
+     * @param int $UserID
47
+     * @return array of BadgeIDs
48
+     */
49
+    public static function get_displayed_badges($UserID)
50
+    {
51
+        $Result = [];
52
+        $Badges = self::get_badges($UserID);
53
+
54
+        foreach ($Badges as $Badge => $Displayed) {
55
+            if ($Displayed) {
56
+                $Result[] = $Badge;
57
+            }
58
+        }
59
+        return $Result;
60
+    }
61
+
62
+
63
+    /**
64
+     * Returns true if the given user owns the given badge
65
+     *
66
+     * @param int $UserID
67
+     * @param int $BadgeID
68
+     * @return bool
69
+     */
70
+    public static function has_badge($UserID, $BadgeID)
71
+    {
72
+        $Badges = self::get_badges($UserID);
73
+        return (array_key_exists($BadgeID, $Badges)) ?: false;
74
+    }
75
+
76
+
77
+    /**
78
+     * Creates HTML for displaying a badge.
79
+     *
80
+     * @param int $BadgeID
81
+     * @param bool $Tooltip Should HTML contain a tooltip?
82
+     * @return string HTML
83
+     */
84
+    public static function display_badge($BadgeID, $Tooltip = false)
85
+    {
86
+        $html = '';
87
+        if (($Badges = G::$Cache->get_value('badges')) && array_key_exists($BadgeID, $Badges)) {
88
+            extract($Badges[$BadgeID]);
89
+        } else {
90
+            self::update_badge_cache();
91
+            if (($Badges = G::$Cache->get_value('badges')) && array_key_exists($BadgeID, $Badges)) {
92
+                extract($Badges[$BadgeID]);
93
+            } else {
94
+                global $Debug;
95
+                $Debug->analysis("Invalid BadgeID $BadgeID requested.");
96
+            }
97
+        }
98
+
99
+        if ($Tooltip) {
100
+            $html .= "<a class='badge_icon'><img class='badge tooltip' alt='$Name' title='$Name: $Description' src='$Icon' /></a>";
101
+        } else {
102
+            $html .= "<a class='badge_icon'><img class='badge' alt='$Name' title='$Name' src='$Icon' /></a>";
103
+        }
104
+
105
+        return $html;
106
+    }
107
+
108
+
109
+    /**
110
+     * display_badges()
111
+     */
112
+    public static function display_badges($BadgeIDs, $Tooltip = false)
113
+    {
114
+        $html = '';
115
+        foreach ($BadgeIDs as $BadgeID) {
116
+            $html .= self::display_badge($BadgeID, $Tooltip);
117
+        }
118
+        return $html;
119
+    }
120
+
121
+
122
+    /**
123
+     * update_badge_cache()
124
+     */
125
+    private static function update_badge_cache()
126
+    {
127
+        $QueryID = G::$DB->get_query_id();
128
+
129
+        G::$DB->query("
130
+        SELECT
131
+          `ID`,
132
+          `Icon`,
133
+          `Name`,
134
+          `Description`
135
+        FROM
136
+          `badges`
137
+        ");
138
+
139
+        $badges = [];
140
+        if (G::$DB->has_results()) {
141
+            while (list($id, $icon, $name, $description) = G::$DB->next_record()) {
142
+                $badges[$id] = array('Icon' => $icon, 'Name' => $name, 'Description' => $description);
143
+            }
144
+            G::$Cache->cache_value('badges', $badges);
145
+        }
146
+
147
+        G::$DB->set_query_id($QueryID);
148
+    }
149
+
150
+
151
+    /**
152
+     * get_all_badges()
153
+     */
154
+    public static function get_all_badges()
155
+    {
156
+        if (($Badges = G::$Cache->get_value('badges'))) {
157
+            return $Badges;
158
+        } else {
159
+            self::update_badge_cache();
160
+            return G::$Cache->get_value('badges');
161
+        }
162
+    }
163
+}

+ 111
- 0
classes/bencode.class.php View File

@@ -0,0 +1,111 @@
1
+<?php
2
+
3
+/**
4
+ * If we're running a 32bit PHP version, we use small objects to store ints.
5
+ * Overhead from the function calls is small enough to not worry about
6
+ */
7
+class Int64
8
+{
9
+    private $Num;
10
+
11
+    public function __construct($Val)
12
+    {
13
+        $this->Num = $Val;
14
+    }
15
+
16
+    public static function make($Val)
17
+    {
18
+        return PHP_INT_SIZE === 4 ? new Int64($Val) : (int)$Val;
19
+    }
20
+
21
+    public static function get($Val)
22
+    {
23
+        return PHP_INT_SIZE === 4 ? $Val->Num : $Val;
24
+    }
25
+
26
+    public static function is_int($Val)
27
+    {
28
+        return is_int($Val) || (is_object($Val) && get_class($Val) === 'Int64');
29
+    }
30
+}
31
+
32
+/**
33
+ * The encode class is simple and straightforward. The only thing to
34
+ * note is that empty dictionaries are represented by boolean trues
35
+ */
36
+class Bencode
37
+{
38
+    private $DefaultKeys = array( // Get rid of everything except these keys to save some space
39
+      'created by', 'creation date', 'encoding', 'info', 'comment');
40
+    private $Data;
41
+    public $Enc;
42
+
43
+    /**
44
+     * Encode an arbitrary array (usually one that's just been decoded)
45
+     *
46
+     * @param array $Arg the thing to encode
47
+     * @param mixed $Keys string or array with keys in the input array to encode or true to encode everything
48
+     * @return bencoded string representing the content of the input array
49
+     */
50
+    public function encode($Arg = false, $Keys = false)
51
+    {
52
+        if ($Arg === false) {
53
+            $Data =& $this->Dec;
54
+        } else {
55
+            $Data =& $Arg;
56
+        }
57
+
58
+        if ($Keys === true) {
59
+            $this->Data = $Data;
60
+        } elseif ($Keys === false) {
61
+            $this->Data = array_intersect_key($Data, array_flip($this->DefaultKeys));
62
+        } elseif (is_array($Keys)) {
63
+            $this->Data = array_intersect_key($Data, array_flip($Keys));
64
+        } else {
65
+            $this->Data = isset($Data[$Keys]) ? $Data[$Keys] : false;
66
+        }
67
+
68
+        if (!$this->Data) {
69
+            return false;
70
+        }
71
+
72
+        $this->Enc = $this->_benc();
73
+        return $this->Enc;
74
+    }
75
+
76
+    /**
77
+     * Internal encoding function that does the actual job
78
+     *
79
+     * @return bencoded string
80
+     */
81
+    private function _benc()
82
+    {
83
+        if (!is_array($this->Data)) {
84
+            if (Int64::is_int($this->Data)) { // Integer
85
+                return 'i'.Int64::get($this->Data).'e';
86
+            }
87
+
88
+            if ($this->Data === true) { // Empty dictionary
89
+                return 'de';
90
+            }
91
+            return strlen($this->Data).':'.$this->Data; // String
92
+        }
93
+
94
+        if (empty($this->Data) || Int64::is_int(key($this->Data))) {
95
+            $IsDict = false;
96
+        } else {
97
+            $IsDict = true;
98
+            ksort($this->Data); // Dictionaries must be sorted
99
+        }
100
+        
101
+        $Ret = $IsDict ? 'd' : 'l';
102
+        foreach ($this->Data as $Key => $Value) {
103
+            if ($IsDict) {
104
+                $Ret .= strlen($Key).':'.$Key;
105
+            }
106
+            $this->Data = $Value;
107
+            $Ret .= $this->_benc();
108
+        }
109
+        return $Ret.'e';
110
+    }
111
+}

+ 208
- 0
classes/bencodedecode.class.php View File

@@ -0,0 +1,208 @@
1
+<?php
2
+
3
+/**
4
+ * The decode class is simple and straightforward. The only thing to
5
+ * note is that empty dictionaries are represented by boolean trues
6
+ */
7
+class BencodeDecode extends Bencode
8
+{
9
+    private $Data;
10
+    private $Length;
11
+    private $Pos = 0;
12
+    public $Dec = [];
13
+    public $ExitOnError = true;
14
+    const SnipLength = 40;
15
+
16
+    /**
17
+     * Decode prepararations
18
+     *
19
+     * @param string $Arg bencoded string or path to bencoded file to decode
20
+     * @param bool $IsPath needs to be true if $Arg is a path
21
+     * @return decoded data with a suitable structure
22
+     */
23
+    public function __construct($Arg = false, $IsPath = false, $Strict = true)
24
+    {
25
+        if (!$Strict) {
26
+            $this->ExitOnError = false;
27
+        }
28
+
29
+        if ($Arg === false) {
30
+            if (empty($this->Enc)) {
31
+                return false;
32
+            }
33
+        } else {
34
+            if ($IsPath === true) {
35
+                return $this->bdec_file($Arg);
36
+            }
37
+            $this->Data = $Arg;
38
+        }
39
+        return $this->decode();
40
+    }
41
+
42
+    /**
43
+     * Decodes a bencoded file
44
+     *
45
+     * @param $Path path to bencoded file to decode
46
+     * @return decoded data with a suitable structure
47
+     */
48
+    public function bdec_file($Path = false)
49
+    {
50
+        if (empty($Path)) {
51
+            return false;
52
+        }
53
+
54
+        if (!$this->Data = @file_get_contents($Path, FILE_BINARY)) {
55
+            return $this->error("Error: file '$Path' could not be opened.\n");
56
+        }
57
+        return $this->decode();
58
+    }
59
+
60
+    /**
61
+     * Decodes a string with bencoded data
62
+     *
63
+     * @param mixed $Arg bencoded data or false to decode the content of $this->Data
64
+     * @return decoded data with a suitable structure
65
+     */
66
+    public function decode($Arg = false)
67
+    {
68
+        if ($Arg !== false) {
69
+            $this->Data = $Arg;
70
+        } elseif (!$this->Data) {
71
+            $this->Data = $this->Enc;
72
+        }
73
+
74
+        if (!$this->Data) {
75
+            return false;
76
+        }
77
+
78
+        $this->Length = strlen($this->Data);
79
+        $this->Pos = 0;
80
+        $this->Dec = $this->_bdec();
81
+
82
+        if ($this->Pos < $this->Length) {
83
+            // Not really necessary, but if the torrent is invalid, it's better to warn than to silently truncate it
84
+            return $this->error();
85
+        }
86
+        return $this->Dec;
87
+    }
88
+
89
+    /**
90
+     * Internal decoding function that does the actual job
91
+     *
92
+     * @return decoded data with a suitable structure
93
+     */
94
+    private function _bdec()
95
+    {
96
+        switch ($this->Data[$this->Pos]) {
97
+
98
+      case 'i':
99
+        $this->Pos++;
100
+        $Value = substr($this->Data, $this->Pos, strpos($this->Data, 'e', $this->Pos) - $this->Pos);
101
+        if (!ctype_digit($Value) && !($Value[0] == '-' && ctype_digit(substr($Value, 1)))) {
102
+            return $this->error();
103
+        }
104
+        $this->Pos += strlen($Value) + 1;
105
+        return Int64::make($Value);
106
+
107
+      case 'l':
108
+        $Value = [];
109
+        $this->Pos++;
110
+        while ($this->Data[$this->Pos] != 'e') {
111
+            if ($this->Pos >= $this->Length) {
112
+                return $this->error();
113
+            }
114
+            $Value[] = $this->_bdec();
115
+        }
116
+        $this->Pos++;
117
+        return $Value;
118
+
119
+      case 'd':
120
+        $Value = [];
121
+        $this->Pos++;
122
+        while ($this->Data[$this->Pos] != 'e') {
123
+            $Length = substr($this->Data, $this->Pos, strpos($this->Data, ':', $this->Pos) - $this->Pos);
124
+            if (!ctype_digit($Length)) {
125
+                return $this->error();
126
+            }
127
+            $this->Pos += strlen($Length) + $Length + 1;
128
+            $Key = substr($this->Data, $this->Pos - $Length, $Length);
129
+            if ($this->Pos >= $this->Length) {
130
+                return $this->error();
131
+            }
132
+            $Value[$Key] = $this->_bdec();
133
+        }
134
+        $this->Pos++;
135
+        // Use boolean true to keep track of empty dictionaries
136
+        return empty($Value) ? true : $Value;
137
+
138
+      default:
139
+        $Length = substr($this->Data, $this->Pos, strpos($this->Data, ':', $this->Pos) - $this->Pos);
140
+        if (!ctype_digit($Length)) {
141
+            return $this->error(); // Even if the string is likely to be decoded correctly without this check, it's malformed
142
+        }
143
+        $this->Pos += strlen($Length) + $Length + 1;
144
+        return substr($this->Data, $this->Pos - $Length, $Length);
145
+    }
146
+    }
147
+
148
+    /**
149
+     * Convert everything to the correct data types and optionally escape strings
150
+     *
151
+     * @param bool $Escape whether to escape the textual data
152
+     * @param mixed $Data decoded data or false to use the $Dec property
153
+     * @return decoded data with more useful data types
154
+     */
155
+    public function dump($Escape = true, $Data = false)
156
+    {
157
+        if ($Data === false) {
158
+            $Data = $this->Dec;
159
+        }
160
+
161
+        if (Int64::is_int($Data)) {
162
+            return Int64::get($Data);
163
+        }
164
+
165
+        if (is_bool($Data)) {
166
+            return [];
167
+        }
168
+
169
+        if (is_array($Data)) {
170
+            $Output = [];
171
+            foreach ($Data as $Key => $Val) {
172
+                $Output[$Key] = $this->dump($Escape, $Val);
173
+            }
174
+            return $Output;
175
+        }
176
+        return $Escape ? htmlentities($Data) : $Data;
177
+    }
178
+
179
+    /**
180
+     * Display an error and halt the operation unless the $ExitOnError property is false
181
+     *
182
+     * @param string $ErrMsg the error message to display
183
+     */
184
+    private function error($ErrMsg = false)
185
+    {
186
+        static $ErrorPos;
187
+        if ($this->Pos === $ErrorPos) {
188
+            // The recursive nature of the class requires this to avoid duplicate error messages
189
+            return false;
190
+        }
191
+
192
+        if ($this->ExitOnError) {
193
+            if ($ErrMsg === false) {
194
+                printf(
195
+                    "Malformed string. Invalid character at pos 0x%X: %s\n",
196
+                    $this->Pos,
197
+                    str_replace(array("\r","\n"), array('',' '), htmlentities(substr($this->Data, $this->Pos, self::SnipLength)))
198
+                );
199
+            } else {
200
+                echo $ErrMsg;
201
+            }
202
+            exit();
203
+        }
204
+        
205
+        $ErrorPos = $this->Pos;
206
+        return false;
207
+    }
208
+}

+ 175
- 0
classes/bencodetorrent.class.php View File

@@ -0,0 +1,175 @@
1
+<?php
2
+
3
+# todo: Replace this with https://github.com/OPSnet/bencode-torrent
4
+
5
+/**
6
+ * Torrent class that contains some convenient functions related to torrent meta data
7
+ */
8
+class BencodeTorrent extends BencodeDecode
9
+{
10
+    private $PathKey = 'path';
11
+    public $Files = [];
12
+    public $Size = 0;
13
+
14
+    /**
15
+     * Create a list of the files in the torrent and their sizes as well as the total torrent size
16
+     *
17
+     * @return array with a list of files and file sizes
18
+     */
19
+    public function file_list()
20
+    {
21
+        if (empty($this->Dec)) {
22
+            return false;
23
+        }
24
+
25
+        $InfoDict =& $this->Dec['info'];
26
+        if (!isset($InfoDict['files'])) {
27
+            // Single-file torrent
28
+            $this->Size = (Int64::is_int($InfoDict['length'])
29
+        ? Int64::get($InfoDict['length'])
30
+        : $InfoDict['length']);
31
+            $Name = (isset($InfoDict['name.utf-8'])
32
+        ? $InfoDict['name.utf-8']
33
+        : $InfoDict['name']);
34
+            $this->Files[] = array($this->Size, $Name);
35
+        } else {
36
+            if (isset($InfoDict['path.utf-8']['files'][0])) {
37
+                $this->PathKey = 'path.utf-8';
38
+            }
39
+
40
+            foreach ($InfoDict['files'] as $File) {
41
+                $TmpPath = [];
42
+                foreach ($File[$this->PathKey] as $SubPath) {
43
+                    $TmpPath[] = $SubPath;
44
+                }
45
+                $CurSize = (Int64::is_int($File['length'])
46
+          ? Int64::get($File['length'])
47
+          : $File['length']);
48
+                $this->Files[] = array($CurSize, implode('/', $TmpPath));
49
+                $this->Size += $CurSize;
50
+            }
51
+            uasort($this->Files, function ($a, $b) {
52
+                return strnatcasecmp($a[1], $b[1]);
53
+            });
54
+        }
55
+        return array($this->Size, $this->Files);
56
+    }
57
+
58
+    /**
59
+     * Find out the name of the torrent
60
+     *
61
+     * @return string torrent name
62
+     */
63
+    public function get_name()
64
+    {
65
+        if (empty($this->Dec)) {
66
+            return false;
67
+        }
68
+        if (isset($this->Dec['info']['name.utf-8'])) {
69
+            return $this->Dec['info']['name.utf-8'];
70
+        }
71
+        return $this->Dec['info']['name'];
72
+    }
73
+
74
+    /**
75
+     * Find out the total size of the torrent
76
+     *
77
+     * @return string torrent size
78
+     */
79
+    public function get_size()
80
+    {
81
+        if (empty($this->Files)) {
82
+            if (empty($this->Dec)) {
83
+                return false;
84
+            }
85
+            $FileList = $this->file_list();
86
+        }
87
+        return $FileList[0];
88
+    }
89
+
90
+    /**
91
+     * Checks if the "private" flag is present in the torrent
92
+     *
93
+     * @return true if the "private" flag is set
94
+     */
95
+    public function is_private()
96
+    {
97
+        if (empty($this->Dec)) {
98
+            return false;
99
+        }
100
+        return isset($this->Dec['info']['private']) && Int64::get($this->Dec['info']['private']) == 1;
101
+    }
102
+    /**
103
+     * Add the "private" flag to the torrent
104
+     *
105
+     * @return true if a change was required
106
+     */
107
+    public function make_private()
108
+    {
109
+        if (empty($this->Dec)) {
110
+            return false;
111
+        }
112
+        if ($this->is_private()) {
113
+            return false;
114
+        }
115
+        $this->Dec['info']['private'] = Int64::make(1);
116
+        ksort($this->Dec['info']);
117
+        return true;
118
+    }
119
+
120
+    /**
121
+     * Add the "source" field to the torrent
122
+     *
123
+     * @return true if a change was required
124
+     */
125
+    public function make_sourced()
126
+    {
127
+        $Sources = Users::get_upload_sources();
128
+        if (empty($this->Dec)) {
129
+            return false;
130
+        }
131
+        if (isset($this->Dec['info']['source']) && ($this->Dec['info']['source'] == $Sources[0] || $this->Dec['info']['source'] == $Sources[1])) {
132
+            return false;
133
+        }
134
+        $this->Dec['info']['source'] = $Sources[0];
135
+        ksort($this->Dec['info']);
136
+        return true;
137
+    }
138
+
139
+    /**
140
+     * Calculate the torrent's info hash
141
+     *
142
+     * @return info hash in hexadecimal form
143
+     */
144
+    public function info_hash()
145
+    {
146
+        if (empty($this->Dec) || !isset($this->Dec['info'])) {
147
+            return false;
148
+        }
149
+        return sha1($this->encode(false, 'info'));
150
+    }
151
+
152
+    /**
153
+     * Add the announce URL to a torrent
154
+     */
155
+    public static function add_announce_url($Data, $Url)
156
+    {
157
+        return 'd8:announce'.strlen($Url).':'.$Url.substr($Data, 1);
158
+    }
159
+
160
+    /**
161
+     * Add list of announce URLs to a torrent
162
+     */
163
+    public static function add_announce_list($Data, $Urls)
164
+    {
165
+        $r = 'd13:announce-listl';
166
+        for ($i = 0; $i < count($Urls); $i++) {
167
+            $r .= 'l';
168
+            for ($j = 0; $j < count($Urls[$i]); $j++) {
169
+                $r .= strlen($Urls[$i][$j]).':'.$Urls[$i][$j];
170
+            }
171
+            $r .= 'e';
172
+        }
173
+        return $r.'e'.substr($Data, 1);
174
+    }
175
+}

+ 39
- 0
classes/bitcoinrpc.class.php View File

@@ -0,0 +1,39 @@
1
+<?php
2
+
3
+class BitcoinRpc
4
+{
5
+    public static function __callStatic($Method, $Args)
6
+    {
7
+        if (!defined('BITCOIN_RPC_URL')) {
8
+            return false;
9
+        }
10
+
11
+        $MessageID = mt_rand();
12
+        $Params = json_encode(
13
+            array(
14
+                'method' => $Method,
15
+                'params' => $Args,
16
+                'id' => $MessageID
17
+            )
18
+        );
19
+
20
+        $Request = array(
21
+            'http' => array(
22
+                'method' => 'POST',
23
+                'header' => 'Content-type: application/json',
24
+                'content' => $Params
25
+            )
26
+        );
27
+
28
+        if (!$Response = file_get_contents(BITCOIN_RPC_URL, false, stream_context_create($Request))) {
29
+            return false;
30
+        }
31
+
32
+        $Response = json_decode($Response);
33
+        if ($Response->id != $MessageID || !empty($Response->error) || empty($Response->result)) {
34
+            return false;
35
+        }
36
+        
37
+        return $Response->result;
38
+    }
39
+}

+ 110
- 0
classes/bookmarks.class.php View File

@@ -0,0 +1,110 @@
1
+<?php
2
+
3
+class Bookmarks
4
+{
5
+    /**
6
+     * Check if can bookmark
7
+     *
8
+     * @param string $Type
9
+     * @return boolean
10
+     */
11
+    public static function can_bookmark($Type)
12
+    {
13
+        return in_array($Type, array(
14
+            'torrent',
15
+            'artist',
16
+            'collage',
17
+            'request'
18
+        ));
19
+    }
20
+
21
+    /**
22
+     * Get the bookmark schema.
23
+     * Recommended usage:
24
+     * list($Table, $Col) = bookmark_schema('torrent');
25
+     *
26
+     * @param string $Type the type to get the schema for
27
+     */
28
+    public static function bookmark_schema($Type)
29
+    {
30
+        switch ($Type) {
31
+          case 'torrent':
32
+              return array(
33
+                  'bookmarks_torrents',
34
+                  'GroupID'
35
+              );
36
+              break;
37
+
38
+          case 'artist':
39
+              return array(
40
+                  'bookmarks_artists',
41
+                  'ArtistID'
42
+              );
43
+              break;
44
+
45
+          case 'collage':
46
+              return array(
47
+                  'bookmarks_collages',
48
+                  'CollageID'
49
+               );
50
+              break;
51
+
52
+          case 'request':
53
+              return array(
54
+                  'bookmarks_requests',
55
+                  'RequestID'
56
+              );
57
+              break;
58
+
59
+          default:
60
+              error('h4x');
61
+        }
62
+    }
63
+
64
+    /**
65
+     * Check if something is bookmarked
66
+     *
67
+     * @param string $Type
68
+     *          type of bookmarks to check
69
+     * @param int $ID
70
+     *          bookmark's id
71
+     * @return boolean
72
+     */
73
+    public static function has_bookmarked($Type, $ID)
74
+    {
75
+        return in_array($ID, self::all_bookmarks($Type));
76
+    }
77
+
78
+    /**
79
+     * Fetch all bookmarks of a certain type for a user.
80
+     * If UserID is false than defaults to G::$LoggedUser['ID']
81
+     *
82
+     * @param string $Type
83
+     *          type of bookmarks to fetch
84
+     * @param int $UserID
85
+     *          userid whose bookmarks to get
86
+     * @return array the bookmarks
87
+     */
88
+    public static function all_bookmarks($Type, $UserID = false)
89
+    {
90
+        if ($UserID === false) {
91
+            $UserID = G::$LoggedUser['ID'];
92
+        }
93
+
94
+        $CacheKey = "bookmarks_$Type".'_'.$UserID;
95
+        if (($Bookmarks = G::$Cache->get_value($CacheKey)) === false) {
96
+            list($Table, $Col) = self::bookmark_schema($Type);
97
+            $QueryID = G::$DB->get_query_id();
98
+
99
+            G::$DB->query("
100
+            SELECT `$Col`
101
+            FROM `$Table`
102
+              WHERE UserID = '$UserID'");
103
+
104
+            $Bookmarks = G::$DB->collect($Col);
105
+            G::$DB->set_query_id($QueryID);
106
+            G::$Cache->cache_value($CacheKey, $Bookmarks, 0);
107
+        }
108
+        return $Bookmarks;
109
+    }
110
+}

+ 5
- 0
classes/browse.class.php View File

@@ -0,0 +1,5 @@
1
+<?php
2
+
3
+class Browse
4
+{
5
+}

+ 476
- 0
classes/cache.class.php View File

@@ -0,0 +1,476 @@
1
+<?php
2
+#declare(strict_types = 1);
3
+
4
+/*************************************************************************|
5
+|--------------- Caching class -------------------------------------------|
6
+|*************************************************************************|
7
+
8
+This class is a wrapper for the Memcache class, and it's been written in
9
+order to better handle the caching of full pages with bits of dynamic
10
+content that are different for every user.
11
+
12
+As this inherits memcache, all of the default memcache methods work -
13
+however, this class has page caching functions superior to those of
14
+memcache.
15
+
16
+Also, Memcache::get and Memcache::set have been wrapped by
17
+Cache::get_value and Cache::cache_value. get_value uses the same argument
18
+as get, but cache_value only takes the key, the value, and the duration
19
+(no zlib).
20
+
21
+// Unix sockets
22
+memcached -d -m 5120 -s /var/run/memcached.sock -a 0777 -t16 -C -u root
23
+
24
+// TCP bind
25
+memcached -d -m 8192 -l 10.10.0.1 -t8 -C
26
+
27
+|*************************************************************************/
28
+
29
+if (!extension_loaded('memcache') && !extension_loaded('memcached')) {
30
+    error('Memcache extension not loaded');
31
+}
32
+
33
+if (class_exists('Memcached')) {
34
+    class MemcacheCompat extends Memcached
35
+    {
36
+    }
37
+} else {
38
+    class MemcacheCompat extends Memcache
39
+    {
40
+    }
41
+}
42
+
43
+class Cache extends MemcacheCompat
44
+{
45
+    // Torrent Group cache version
46
+    const GROUP_VERSION = 5;
47
+
48
+    public $CacheHits = [];
49
+    public $MemcacheDBArray = [];
50
+    public $MemcacheDBKey = '';
51
+    protected $InTransaction = false;
52
+    public $Time = 0;
53
+    private $Servers = [];
54
+    private $PersistentKeys = [
55
+        'ajax_requests_*',
56
+        'query_lock_*',
57
+        'stats_*',
58
+        'top10tor_*',
59
+        'users_snatched_*',
60
+
61
+            // Cache-based features
62
+            'global_notification',
63
+            'notifications_one_reads_*',
64
+    ];
65
+    private $ClearedKeys = [];
66
+    public $CanClear = false;
67
+    public $InternalCache = true;
68
+
69
+    public function __construct($Servers)
70
+    {
71
+        if (is_subclass_of($this, 'Memcached')) {
72
+            parent::__construct();
73
+        }
74
+
75
+        $this->Servers = $Servers;
76
+        foreach ($Servers as $Server) {
77
+            if (is_subclass_of($this, 'Memcache')) {
78
+                $this->addServer($Server['host'], $Server['port'], true, $Server['buckets']);
79
+            } else {
80
+                $this->addServer(str_replace('unix://', '', $Server['host']), $Server['port'], $Server['buckets']);
81
+            }
82
+        }
83
+    }
84
+
85
+    //---------- Caching functions ----------//
86
+
87
+    // Allows us to set an expiration on otherwise perminantly cache'd values
88
+    // Useful for disabled users, locked threads, basically reducing ram usage
89
+    public function expire_value($Key, $Duration = 2592000)
90
+    {
91
+        $StartTime = microtime(true);
92
+        $this->set($Key, $this->get($Key), $Duration);
93
+        $this->Time += (microtime(true) - $StartTime) * 1000;
94
+    }
95
+
96
+    // Wrapper for Memcache::set, with the zlib option removed and default duration of 30 days
97
+    public function cache_value($Key, $Value, $Duration = 2592000)
98
+    {
99
+        $StartTime = microtime(true);
100
+        if (empty($Key)) {
101
+            trigger_error('Cache insert failed for empty key');
102
+        }
103
+
104
+        $SetParams = [$Key, $Value, 0, $Duration];
105
+        if (is_subclass_of($this, 'Memcached')) {
106
+            unset($SetParams[2]);
107
+        }
108
+
109
+        if (!$this->set(...$SetParams)) {
110
+            trigger_error("Cache insert failed for key $Key");
111
+        }
112
+
113
+        if ($this->InternalCache && array_key_exists($Key, $this->CacheHits)) {
114
+            $this->CacheHits[$Key] = $Value;
115
+        }
116
+        $this->Time += (microtime(true) - $StartTime) * 1000;
117
+    }
118
+
119
+    // Wrapper for Memcache::add, with the zlib option removed and default duration of 30 days
120
+    public function add_value($Key, $Value, $Duration = 2592000)
121
+    {
122
+        $StartTime = microtime(true);
123
+        $Added = $this->add($Key, $Value, 0, $Duration);
124
+        $this->Time += (microtime(true) - $StartTime) * 1000;
125
+        return $Added;
126
+    }
127
+
128
+    public function replace_value($Key, $Value, $Duration = 2592000)
129
+    {
130
+        $StartTime = microtime(true);
131
+        $ReplaceParams = [$Key, $Value, false, $Duration];
132
+
133
+        if (is_subclass_of($this, 'Memcached')) {
134
+            unset($ReplaceParams[2]);
135
+        }
136
+        $this->replace(...$ReplaceParams);
137
+
138
+        if ($this->InternalCache && array_key_exists($Key, $this->CacheHits)) {
139
+            $this->CacheHits[$Key] = $Value;
140
+        }
141
+        $this->Time += (microtime(true) - $StartTime) * 1000;
142
+    }
143
+
144
+    public function get_value($Key, $NoCache = false)
145
+    {
146
+        if (!$this->InternalCache) {
147
+            $NoCache = true;
148
+        }
149
+
150
+        $StartTime = microtime(true);
151
+        if (empty($Key)) {
152
+            trigger_error('Cache retrieval failed for empty key');
153
+        }
154
+
155
+        if (!empty($_GET['clearcache']) && $this->CanClear && !isset($this->ClearedKeys[$Key]) && !Misc::in_array_partial($Key, $this->PersistentKeys)) {
156
+            if ($_GET['clearcache'] === '1') {
157
+                // Because check_perms() isn't true until LoggedUser is pulled from the cache, we have to remove the entries loaded before the LoggedUser data
158
+                // Because of this, not user cache data will require a secondary pageload following the clearcache to update
159
+                if (count($this->CacheHits) > 0) {
160
+                    foreach (array_keys($this->CacheHits) as $HitKey) {
161
+                        if (!isset($this->ClearedKeys[$HitKey]) && !Misc::in_array_partial($HitKey, $this->PersistentKeys)) {
162
+                            $this->delete($HitKey);
163
+                            unset($this->CacheHits[$HitKey]);
164
+                            $this->ClearedKeys[$HitKey] = true;
165
+                        }
166
+                    }
167
+                }
168
+
169
+                $this->delete($Key);
170
+                $this->Time += (microtime(true) - $StartTime) * 1000;
171
+                return false;
172
+            } elseif ($_GET['clearcache'] === $Key) {
173
+                $this->delete($Key);
174
+                $this->Time += (microtime(true) - $StartTime) * 1000;
175
+                return false;
176
+            } elseif (substr($_GET['clearcache'], -1) === '*') {
177
+                $Prefix = substr($_GET['clearcache'], 0, -1);
178
+                if ($Prefix === '' || $Prefix === substr($Key, 0, strlen($Prefix))) {
179
+                    $this->delete($Key);
180
+                    $this->Time += (microtime(true) - $StartTime) * 1000;
181
+                    return false;
182
+                }
183
+            }
184
+            $this->ClearedKeys[$Key] = true;
185
+        }
186
+
187
+        // For cases like the forums, if a key is already loaded, grab the existing pointer
188
+        if (isset($this->CacheHits[$Key]) && !$NoCache) {
189
+            $this->Time += (microtime(true) - $StartTime) * 1000;
190
+            return $this->CacheHits[$Key];
191
+        }
192
+
193
+        $Return = $this->get($Key);
194
+        if ($Return !== false) {
195
+            $this->CacheHits[$Key] = $NoCache ? null : $Return;
196
+        }
197
+
198
+        $this->Time += (microtime(true) - $StartTime) * 1000;
199
+        return $Return;
200
+    }
201
+
202
+    // Wrapper for Memcache::delete. For a reason, see above.
203
+    public function delete_value($Key)
204
+    {
205
+        $StartTime = microtime(true);
206
+        if (empty($Key)) {
207
+            trigger_error('Cache deletion failed for empty key');
208
+        }
209
+
210
+        if (!$this->delete($Key)) {
211
+            //trigger_error("Cache delete failed for key $Key");
212
+        }
213
+
214
+        unset($this->CacheHits[$Key]);
215
+        $this->Time += (microtime(true) - $StartTime) * 1000;
216
+    }
217
+
218
+    public function increment_value($Key, $Value = 1)
219
+    {
220
+        $StartTime = microtime(true);
221
+        $NewVal = $this->increment($Key, $Value);
222
+
223
+        if (isset($this->CacheHits[$Key])) {
224
+            $this->CacheHits[$Key] = $NewVal;
225
+        }
226
+        $this->Time += (microtime(true) - $StartTime) * 1000;
227
+    }
228
+
229
+    public function decrement_value($Key, $Value = 1)
230
+    {
231
+        $StartTime = microtime(true);
232
+        $NewVal = $this->decrement($Key, $Value);
233
+
234
+        if (isset($this->CacheHits[$Key])) {
235
+            $this->CacheHits[$Key] = $NewVal;
236
+        }
237
+        $this->Time += (microtime(true) - $StartTime) * 1000;
238
+    }
239
+
240
+    //---------- memcachedb functions ----------//
241
+
242
+    public function begin_transaction($Key)
243
+    {
244
+        $Value = $this->get($Key);
245
+        if (!is_array($Value)) {
246
+            $this->InTransaction = false;
247
+            $this->MemcacheDBKey = [];
248
+            $this->MemcacheDBKey = '';
249
+            return false;
250
+        }
251
+
252
+        $this->MemcacheDBArray = $Value;
253
+        $this->MemcacheDBKey = $Key;
254
+        $this->InTransaction = true;
255
+        return true;
256
+    }
257
+
258
+    public function cancel_transaction()
259
+    {
260
+        $this->InTransaction = false;
261
+        $this->MemcacheDBKey = [];
262
+        $this->MemcacheDBKey = '';
263
+    }
264
+
265
+    public function commit_transaction($Time = 2592000)
266
+    {
267
+        if (!$this->InTransaction) {
268
+            return false;
269
+        }
270
+
271
+        $this->cache_value($this->MemcacheDBKey, $this->MemcacheDBArray, $Time);
272
+        $this->InTransaction = false;
273
+    }
274
+
275
+    // Updates multiple rows in an array
276
+    public function update_transaction($Rows, $Values)
277
+    {
278
+        if (!$this->InTransaction) {
279
+            return false;
280
+        }
281
+
282
+        $Array = $this->MemcacheDBArray;
283
+        if (is_array($Rows)) {
284
+            $i = 0;
285
+            $Keys = $Rows[0];
286
+            $Property = $Rows[1];
287
+            foreach ($Keys as $Row) {
288
+                $Array[$Row][$Property] = $Values[$i];
289
+                $i++;
290
+            }
291
+        } else {
292
+            $Array[$Rows] = $Values;
293
+        }
294
+        $this->MemcacheDBArray = $Array;
295
+    }
296
+
297
+    // Updates multiple values in a single row in an array
298
+    // $Values must be an associative array with key:value pairs like in the array we're updating
299
+    public function update_row($Row, $Values)
300
+    {
301
+        if (!$this->InTransaction) {
302
+            return false;
303
+        }
304
+
305
+        if ($Row === false) {
306
+            $UpdateArray = $this->MemcacheDBArray;
307
+        } else {
308
+            $UpdateArray = $this->MemcacheDBArray[$Row];
309
+        }
310
+
311
+        foreach ($Values as $Key => $Value) {
312
+            if (!array_key_exists($Key, $UpdateArray)) {
313
+                trigger_error('Bad transaction key ('.$Key.') for cache '.$this->MemcacheDBKey);
314
+            }
315
+
316
+            if ($Value === '+1') {
317
+                if (!is_number($UpdateArray[$Key])) {
318
+                    trigger_error('Tried to increment non-number ('.$Key.') for cache '.$this->MemcacheDBKey);
319
+                }
320
+                ++$UpdateArray[$Key]; // Increment value
321
+            } elseif ($Value === '-1') {
322
+                if (!is_number($UpdateArray[$Key])) {
323
+                    trigger_error('Tried to decrement non-number ('.$Key.') for cache '.$this->MemcacheDBKey);
324
+                }
325
+                --$UpdateArray[$Key]; // Decrement value
326
+            } else {
327
+                $UpdateArray[$Key] = $Value; // Otherwise, just alter value
328
+            }
329
+        }
330
+
331
+        if ($Row === false) {
332
+            $this->MemcacheDBArray = $UpdateArray;
333
+        } else {
334
+            $this->MemcacheDBArray[$Row] = $UpdateArray;
335
+        }
336
+    }
337
+
338
+    // Increments multiple values in a single row in an array
339
+    // $Values must be an associative array with key:value pairs like in the array we're updating
340
+    public function increment_row($Row, $Values)
341
+    {
342
+        if (!$this->InTransaction) {
343
+            return false;
344
+        }
345
+
346
+        if ($Row === false) {
347
+            $UpdateArray = $this->MemcacheDBArray;
348
+        } else {
349
+            $UpdateArray = $this->MemcacheDBArray[$Row];
350
+        }
351
+
352
+        foreach ($Values as $Key => $Value) {
353
+            if (!array_key_exists($Key, $UpdateArray)) {
354
+                trigger_error("Bad transaction key ($Key) for cache ".$this->MemcacheDBKey);
355
+            }
356
+
357
+            if (!is_number($Value)) {
358
+                trigger_error("Tried to increment with non-number ($Key) for cache ".$this->MemcacheDBKey);
359
+            }
360
+            $UpdateArray[$Key] += $Value; // Increment value
361
+        }
362
+
363
+        if ($Row === false) {
364
+            $this->MemcacheDBArray = $UpdateArray;
365
+        } else {
366
+            $this->MemcacheDBArray[$Row] = $UpdateArray;
367
+        }
368
+    }
369
+
370
+    // Insert a value at the beginning of the array
371
+    public function insert_front($Key, $Value)
372
+    {
373
+        if (!$this->InTransaction) {
374
+            return false;
375
+        }
376
+
377
+        if ($Key === '') {
378
+            array_unshift($this->MemcacheDBArray, $Value);
379
+        } else {
380
+            $this->MemcacheDBArray = array($Key=>$Value) + $this->MemcacheDBArray;
381
+        }
382
+    }
383
+
384
+    // Insert a value at the end of the array
385
+    public function insert_back($Key, $Value)
386
+    {
387
+        if (!$this->InTransaction) {
388
+            return false;
389
+        }
390
+
391
+        if ($Key === '') {
392
+            array_push($this->MemcacheDBArray, $Value);
393
+        } else {
394
+            $this->MemcacheDBArray = $this->MemcacheDBArray + array($Key=>$Value);
395
+        }
396
+    }
397
+
398
+    public function insert($Key, $Value)
399
+    {
400
+        if (!$this->InTransaction) {
401
+            return false;
402
+        }
403
+
404
+        if ($Key === '') {
405
+            $this->MemcacheDBArray[] = $Value;
406
+        } else {
407
+            $this->MemcacheDBArray[$Key] = $Value;
408
+        }
409
+    }
410
+
411
+    public function delete_row($Row)
412
+    {
413
+        if (!$this->InTransaction) {
414
+            return false;
415
+        }
416
+
417
+        if (!isset($this->MemcacheDBArray[$Row])) {
418
+            trigger_error("Tried to delete non-existent row ($Row) for cache ".$this->MemcacheDBKey);
419
+        }
420
+        unset($this->MemcacheDBArray[$Row]);
421
+    }
422
+
423
+    public function update($Key, $Rows, $Values, $Time = 2592000)
424
+    {
425
+        if (!$this->InTransaction) {
426
+            $this->begin_transaction($Key);
427
+            $this->update_transaction($Rows, $Values);
428
+            $this->commit_transaction($Time);
429
+        } else {
430
+            $this->update_transaction($Rows, $Values);
431
+        }
432
+    }
433
+
434
+    /**
435
+     * Tries to set a lock. Expiry time is one hour to avoid indefinite locks
436
+     *
437
+     * @param string $LockName name on the lock
438
+     * @return true if lock was acquired
439
+     */
440
+    public function get_query_lock($LockName)
441
+    {
442
+        return $this->add_value('query_lock_'.$LockName, 1, 3600);
443
+    }
444
+
445
+    /**
446
+     * Remove lock
447
+     *
448
+     * @param string $LockName name on the lock
449
+     */
450
+    public function clear_query_lock($LockName)
451
+    {
452
+        $this->delete_value('query_lock_'.$LockName);
453
+    }
454
+
455
+    /**
456
+     * Get cache server status
457
+     *
458
+     * @return array (host => int status, ...)
459
+     */
460
+    public function server_status()
461
+    {
462
+        $Status = [];
463
+        if (is_subclass_of($this, 'Memcached')) {
464
+            $MemcachedStats = $this->getStats();
465
+        }
466
+        
467
+        foreach ($this->Servers as $Server) {
468
+            if (is_subclass_of($this, 'Memcached')) {
469
+                $Status["$Server[host]:$Server[port]"] = gettype($MemcachedStats["$Server[host]:$Server[port]"]) === 'array' ? 1 : 0;
470
+            } else {
471
+                $Status["$Server[host]:$Server[port]"] = $this->getServerStatus($Server['host'], $Server['port']);
472
+            }
473
+        }
474
+        return $Status;
475
+    }
476
+}

+ 232
- 0
classes/charts.class.php View File

@@ -0,0 +1,232 @@
1
+<?php
2
+
3
+class GOOGLE_CHARTS
4
+{
5
+    protected $URL = 'https://chart.googleapis.com/chart';
6
+    protected $Labels = [];
7
+    protected $Data = [];
8
+    protected $Options = [];
9
+
10
+    public function __construct($Type, $Width, $Height, $Options)
11
+    {
12
+        if ($Width * $Height > 300000 || $Height > 1000 || $Width > 1000) {
13
+            trigger_error('Tried to make chart too large.');
14
+        }
15
+
16
+        $this->URL .= "?cht=$Type&amp;chs={$Width}x$Height";
17
+        $this->Options = $Options;
18
+    }
19
+
20
+    protected function encode($Number)
21
+    {
22
+        if ($Number === -1) {
23
+            return '__';
24
+        }
25
+
26
+        $CharKey = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.';
27
+        return $CharKey[floor($Number / 64)].$CharKey[floor($Number % 64)];
28
+    }
29
+
30
+    public function color($Colors)
31
+    {
32
+        $this->URL .= '&amp;chco='.$Colors;
33
+    }
34
+
35
+    public function lines($Thickness, $Solid = 1, $Blank = 0)
36
+    {
37
+        $this->URL .= "&amp;chls=$Thickness,$Solid,$Blank";
38
+    }
39
+
40
+    public function title($Title, $Color = '', $Size = '')
41
+    {
42
+        $this->URL .= '&amp;chtt='.str_replace(array(' ', "\n"), array('+', '|'), $Title);
43
+        if (!empty($Color)) {
44
+            $this->URL .= '&amp;chts='.$Color;
45
+        }
46
+
47
+        if (!empty($Size)) {
48
+            $this->URL .= ','.$Size;
49
+        }
50
+    }
51
+
52
+    public function legend($Items, $Placement = '')
53
+    {
54
+        $this->URL .= '&amp;chdl='.str_replace(' ', '+', implode('|', $Items));
55
+        if (!empty($Placement)) {
56
+            if (!in_array($Placement, array('b', 't', 'r', 'l', 'bv', 'tv'))) {
57
+                trigger_error('Invalid legend placement.');
58
+            }
59
+            $this->URL .= '&amp;chdlp='.$Placement;
60
+        }
61
+    }
62
+
63
+    public function add($Label, $Data)
64
+    {
65
+        if ($Label !== false) {
66
+            $this->Labels[] = $Label;
67
+        }
68
+        $this->Data[] = $Data;
69
+    }
70
+
71
+    public function grid_lines($SpacingX = 0, $SpacingY = -1, $Solid = 1, $Blank = 1)
72
+    {
73
+        // Can take 2 more parameters for offset, but we're not bothering with that right now
74
+        $this->URL .= "&amp;chg=$SpacingX,$SpacingY,$Solid,$Blank";
75
+    }
76
+
77
+    public function transparent()
78
+    {
79
+        $this->URL .= '&amp;chf=bg,s,FFFFFF00';
80
+    }
81
+
82
+
83
+    public function url()
84
+    {
85
+        return $this->URL;
86
+    }
87
+}
88
+
89
+class AREA_GRAPH extends GOOGLE_CHARTS
90
+{
91
+    public function __construct($Width, $Height, $Options = [])
92
+    {
93
+        parent::__construct('lc', $Width, $Height, $Options);
94
+    }
95
+
96
+    public function color($Color)
97
+    {
98
+        $this->URL .= '&amp;chco='.$Color.'&amp;chm=B,'.$Color.'50,0,0,0';
99
+    }
100
+
101
+    public function generate()
102
+    {
103
+        $Max = max($this->Data);
104
+        $Min = ((isset($this->Options['Break'])) ? $Min = min($this->Data) : 0);
105
+        $Data = [];
106
+
107
+        foreach ($this->Data as $Value) {
108
+            $Data[] = $this->encode((($Value - $Min) / ($Max - $Min)) * 4095);
109
+        }
110
+        $this->URL .= "&amp;chxt=y,x&amp;chxs=0,h&amp;chxl=1:|".implode('|', $this->Labels).'&amp;chxr=0,'.$Min.','.($Max - $Min).'&amp;chd=e:'.implode('', $Data);
111
+    }
112
+}
113
+
114
+class PIE_CHART extends GOOGLE_CHARTS
115
+{
116
+    public function __construct($Width, $Height, $Options = [])
117
+    {
118
+        $Type = ((isset($this->Options['3D'])) ? 'p3' : 'p');
119
+        parent::__construct($Type, $Width, $Height, $Options);
120
+    }
121
+
122
+    public function generate()
123
+    {
124
+        $Sum = array_sum($this->Data);
125
+        $Other = isset($this->Options['Other']);
126
+        $Sort = isset($this->Options['Sort']);
127
+        $LabelPercent = isset($this->Options['Percentage']);
128
+
129
+        if ($Sort && !empty($this->Labels)) {
130
+            array_multisort($this->Data, SORT_DESC, $this->Labels);
131
+        } elseif ($Sort) {
132
+            sort($this->Data);
133
+            $this->Data = array_reverse($this->Data);
134
+        }
135
+
136
+        $Data = [];
137
+        $Labels = $this->Labels;
138
+        $OtherPercentage = 0.00;
139
+        $OtherData = 0;
140
+
141
+        foreach ($this->Data as $Key => $Value) {
142
+            $ThisPercentage = number_format(($Value / $Sum) * 100, 2);
143
+            $ThisData = ($Value / $Sum) * 4095;
144
+
145
+            if ($Other && $ThisPercentage < 1) {
146
+                $OtherPercentage += $ThisPercentage;
147
+                $OtherData += $ThisData;
148
+                unset($Data[$Key]);
149
+                unset($Labels[$Key]);
150
+                continue;
151
+            }
152
+
153
+            if ($LabelPercent) {
154
+                $Labels[$Key] .= ' ('.$ThisPercentage.'%)';
155
+            }
156
+            $Data[] = $this->encode($ThisData);
157
+        }
158
+
159
+        if ($OtherPercentage > 0) {
160
+            $OtherLabel = 'Other';
161
+            if ($LabelPercent) {
162
+                $OtherLabel .= ' ('.$OtherPercentage.'%)';
163
+            }
164
+            $Labels[] = $OtherLabel;
165
+            $Data[] = $this->encode($OtherData);
166
+        }
167
+        $this->URL .= "&amp;chl=".implode('|', $Labels).'&amp;chd=e:'.implode('', $Data);
168
+    }
169
+}
170
+
171
+/*
172
+class LOG_BAR_GRAPH extends GOOGLE_CHARTS
173
+{
174
+    // todo: Finish
175
+    public function __construct($Base, $Width, $Height, $Options = [])
176
+    {
177
+        parent::__construct('lc', $Width, $Height, $Options);
178
+    }
179
+
180
+    public function color($Color)
181
+    {
182
+        $this->URL .= '&amp;chco='.$Color.'&amp;chm=B,'.$Color.'50,0,0,0';
183
+    }
184
+
185
+    public function generate()
186
+    {
187
+        $Max = max($this->Data);
188
+        $Min = ((isset($this->Options['Break'])) ? $Min = min($this->Data) : 0);
189
+        $Data = [];
190
+
191
+        foreach ($this->Data as $Value) {
192
+            $Data[] = $this->encode((($Value - $Min) / ($Max - $Min)) * 4095);
193
+        }
194
+        $this->URL .= "&amp;chxt=y,x&amp;chxs=0,h&amp;chxl=1:|".implode('|', $this->Labels).'&amp;chxr=0,'.$Min.','.($Max-$Min).'&amp;chd=e:'.implode('', $Data);
195
+    }
196
+}
197
+*/
198
+
199
+/*
200
+class POLL_GRAPH extends GOOGLE_CHARTS
201
+{
202
+    public function __construct()
203
+    {
204
+        $this->URL .= '?cht=bhg';
205
+    }
206
+
207
+    public function add($Label, $Data)
208
+    {
209
+        if ($Label !== false) {
210
+            $this->Labels[] = Format::cut_string($Label, 35);
211
+        }
212
+        $this->Data[] = $Data;
213
+    }
214
+
215
+    public function generate()
216
+    {
217
+        $Count = count($this->Data);
218
+        $Height = (30 * $Count) + 20;
219
+        $Max = max($this->Data);
220
+        $Sum = array_sum($this->Data);
221
+        $Increment = ($Max / $Sum) * 25; // * 100% / 4divisions
222
+        $Data = [];
223
+        $Labels = [];
224
+
225
+        foreach ($this->Data as $Key => $Value) {
226
+            $Data[] = $this->encode(($Value / $Max) * 4095);
227
+            $Labels[] = '@t'.str_replace(array(' ', ','), array('+', '\,'), $this->Labels[$Key]).',000000,1,'.round((($Key + 1) / $Count) - (12 / $Height), 2).':0,12';
228
+        }
229
+        $this->URL .= "&amp;chbh=25,0,5&amp;chs=214x$Height&amp;chl=0%|".round($Increment, 1)."%|".round($Increment * 2, 1)."%|".round($Increment * 3, 1)."%|".round($Increment * 4, 1)."%&amp;chm=".implode('|', $Labels).'&amp;chd=e:'.implode('', $Data);
230
+    }
231
+}
232
+*/

+ 78
- 0
classes/collages.class.php View File

@@ -0,0 +1,78 @@
1
+<?php
2
+
3
+class Collages
4
+{
5
+    public static function increase_subscriptions($CollageID)
6
+    {
7
+        $QueryID = G::$DB->get_query_id();
8
+        G::$DB->query("
9
+        UPDATE
10
+          `collages`
11
+        SET
12
+          `Subscribers` = `Subscribers` + 1
13
+        WHERE
14
+          `ID` = '$CollageID'
15
+        ");
16
+        G::$DB->set_query_id($QueryID);
17
+    }
18
+
19
+    public static function decrease_subscriptions($CollageID)
20
+    {
21
+        $QueryID = G::$DB->get_query_id();
22
+        G::$DB->query("
23
+        UPDATE
24
+          `collages`
25
+        SET
26
+          `Subscribers` = IF(
27
+            `Subscribers` < 1,
28
+            0,
29
+            `Subscribers` - 1
30
+          )
31
+        WHERE
32
+          `ID` = '$CollageID'
33
+        ");
34
+        G::$DB->set_query_id($QueryID);
35
+    }
36
+
37
+    public static function create_personal_collage()
38
+    {
39
+        G::$DB->query("
40
+        SELECT
41
+          COUNT(`ID`)
42
+        FROM
43
+          `collages`
44
+        WHERE
45
+          `UserID` = '".G::$LoggedUser['ID']."' AND `CategoryID` = '0' AND `Deleted` = '0'
46
+        ");
47
+        list($CollageCount) = G::$DB->next_record();
48
+
49
+        if ($CollageCount >= G::$LoggedUser['Permissions']['MaxCollages']) {
50
+            // todo: Fix this, the query was for COUNT(ID), so I highly doubt that this works... - Y
51
+            list($CollageID) = G::$DB->next_record();
52
+            header("Location: collage.php?id=$CollageID");
53
+            error();
54
+        }
55
+
56
+        $NameStr = db_string(G::$LoggedUser['Username']."'s personal collage".($CollageCount > 0 ? ' no. '.($CollageCount + 1) : ''));
57
+        $Description = db_string('Personal collage for '.G::$LoggedUser['Username'].'. The first 5 albums will appear on his or her [url='.site_url().'user.php?id= '.G::$LoggedUser['ID'].']profile[/url].');
58
+
59
+        G::$DB->query("
60
+        INSERT INTO `collages`(
61
+          `Name`,
62
+          `Description`,
63
+          `CategoryID`,
64
+          `UserID`
65
+        )
66
+        VALUES(
67
+          '$NameStr',
68
+          '$Description',
69
+          '0',
70
+          ".G::$LoggedUser['ID']."
71
+        )
72
+        ");
73
+          
74
+        $CollageID = G::$DB->inserted_id();
75
+        header("Location: collage.php?id=$CollageID");
76
+        error();
77
+    }
78
+}

+ 573
- 0
classes/comments.class.php View File

@@ -0,0 +1,573 @@
1
+<?php
2
+
3
+class Comments