Browse Source

Initial commit

spaghetti 9 years ago
commit
3d9458b3d0
100 changed files with 13000 additions and 0 deletions
  1. 0
    0
      README.md
  2. 1
    0
      ajax.php
  3. 1
    0
      announce.php
  4. 3
    0
      artist.php
  5. 3
    0
      better.php
  6. 1
    0
      blog.php
  7. 3
    0
      bookmarks.php
  8. 1
    0
      browse.php
  9. 1
    0
      captcha.php
  10. BIN
      captcha/captcha1.png
  11. BIN
      captcha/captcha2.png
  12. BIN
      captcha/captcha3.png
  13. BIN
      captcha/captcha4.png
  14. BIN
      captcha/captcha5.png
  15. BIN
      captcha/captcha6.png
  16. BIN
      captcha/captcha7.png
  17. BIN
      captcha/captcha8.png
  18. BIN
      captcha/captcha9.png
  19. 2
    0
      chat.php
  20. BIN
      classes/.users.class.php.swp
  21. 256
    0
      classes/NMA_API.php
  22. 3
    0
      classes/artist.class.php
  23. 209
    0
      classes/artists.class.php
  24. 385
    0
      classes/artists_similar.class.php
  25. 361
    0
      classes/autoenable.class.php
  26. 145
    0
      classes/badges.class.php
  27. 96
    0
      classes/bencode.class.php
  28. 184
    0
      classes/bencodedecode.class.php
  29. 143
    0
      classes/bencodetorrent.class.php
  30. 32
    0
      classes/bitcoinrpc.class.php
  31. 99
    0
      classes/bookmarks.class.php
  32. 394
    0
      classes/cache.class.php
  33. 140
    0
      classes/calendar.class.php
  34. 119
    0
      classes/calendarview.class.php
  35. 193
    0
      classes/charts.class.php
  36. 35
    0
      classes/classloader.php
  37. 48
    0
      classes/collages.class.php
  38. 441
    0
      classes/comments.class.php
  39. 95
    0
      classes/commentsview.class.php
  40. 276
    0
      classes/config.template
  41. 48
    0
      classes/cookie.class.php
  42. 37
    0
      classes/dbcrypt.class.php
  43. 675
    0
      classes/debug.class.php
  44. 948
    0
      classes/donations.class.php
  45. 186
    0
      classes/donationsbitcoin.class.php
  46. 214
    0
      classes/donationsview.class.php
  47. 34
    0
      classes/encrypt.class.php
  48. 73
    0
      classes/feed.class.php
  49. 78
    0
      classes/file_checker.class.php
  50. BIN
      classes/fonts/ARIBLK.TTF
  51. BIN
      classes/fonts/COMIC.TTF
  52. BIN
      classes/fonts/COMICBD.TTF
  53. BIN
      classes/fonts/GEORGIA.TTF
  54. BIN
      classes/fonts/GEORGIAB.TTF
  55. BIN
      classes/fonts/GEORGIAI.TTF
  56. BIN
      classes/fonts/GEORGIAZ.TTF
  57. BIN
      classes/fonts/IMPACT.TTF
  58. BIN
      classes/fonts/MISTRAL.TTF
  59. 107
    0
      classes/fonts/README.TXT
  60. BIN
      classes/fonts/TREBUC.TTF
  61. BIN
      classes/fonts/TREBUCBD.TTF
  62. BIN
      classes/fonts/TREBUCBI.TTF
  63. BIN
      classes/fonts/TREBUCIT.TTF
  64. BIN
      classes/fonts/VERDANA.TTF
  65. BIN
      classes/fonts/VERDANAB.TTF
  66. BIN
      classes/fonts/VERDANAI.TTF
  67. BIN
      classes/fonts/VERDANAZ.TTF
  68. 580
    0
      classes/format.class.php
  69. 325
    0
      classes/forums.class.php
  70. 13
    0
      classes/g.class.php
  71. 58
    0
      classes/image.class.php
  72. 251
    0
      classes/imagetools.class.php
  73. 31
    0
      classes/inbox.class.php
  74. 245
    0
      classes/invite_tree.class.php
  75. 183
    0
      classes/irc.class.php
  76. 60
    0
      classes/lockedaccounts.class.php
  77. 81
    0
      classes/mass_user_bookmarks_editor.class.php
  78. 61
    0
      classes/mass_user_torrents_editor.class.php
  79. 266
    0
      classes/mass_user_torrents_table_view.class.php
  80. 680
    0
      classes/mediainfo.class.php
  81. 545
    0
      classes/misc.class.php
  82. 414
    0
      classes/mysql.class.php
  83. 800
    0
      classes/notificationsmanager.class.php
  84. 156
    0
      classes/notificationsmanagerview.class.php
  85. 103
    0
      classes/paranoia.class.php
  86. 108
    0
      classes/permissions.class.php
  87. 279
    0
      classes/permissions_form.php
  88. 43
    0
      classes/proxies.class.php
  89. 184
    0
      classes/pushserver.class.php
  90. 16
    0
      classes/regex.php
  91. 27
    0
      classes/reports.class.php
  92. 235
    0
      classes/requests.class.php
  93. 29
    0
      classes/revisionhistory.class.php
  94. 39
    0
      classes/revisionhistoryview.class.php
  95. 171
    0
      classes/rules.class.php
  96. 427
    0
      classes/script_start.php
  97. 269
    0
      classes/sitehistory.class.php
  98. 226
    0
      classes/sitehistoryview.class.php
  99. 25
    0
      classes/siteoptions.class.php
  100. 0
    0
      classes/sphinxql.class.php

+ 0
- 0
README.md View File


+ 1
- 0
ajax.php View File

1
+<? require('classes/script_start.php'); ?>

+ 1
- 0
announce.php View File

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

+ 3
- 0
artist.php View File

1
+<?
2
+define('ERROR_EXCEPTION', true);
3
+require ('classes/script_start.php');

+ 3
- 0
better.php View File

1
+<?
2
+define('ERROR_EXCEPTION', true);
3
+require('classes/script_start.php');

+ 1
- 0
blog.php View File

1
+<? require('classes/script_start.php');

+ 3
- 0
bookmarks.php View File

1
+<?
2
+define('ERROR_EXCEPTION', true);
3
+require('classes/script_start.php');

+ 1
- 0
browse.php View File

1
+<? header('Location: torrents.php');

+ 1
- 0
captcha.php View File

1
+<? require("classes/script_start.php");

BIN
captcha/captcha1.png View File


BIN
captcha/captcha2.png View File


BIN
captcha/captcha3.png View File


BIN
captcha/captcha4.png View File


BIN
captcha/captcha5.png View File


BIN
captcha/captcha6.png View File


BIN
captcha/captcha7.png View File


BIN
captcha/captcha8.png View File


BIN
captcha/captcha9.png View File


+ 2
- 0
chat.php View File

1
+<? require("classes/script_start.php");
2
+

BIN
classes/.users.class.php.swp View File


+ 256
- 0
classes/NMA_API.php View File

1
+<?php
2
+
3
+class NMA_API
4
+{
5
+
6
+    /**
7
+     * @const LIB_ERROR_TYPE can be exception or error
8
+     */
9
+    const LIB_ERROR_TYPE = 'error';
10
+
11
+    /**
12
+     * @const holds the api key verify url
13
+     */
14
+    const LIB_NMA_VERIFY = 'https://www.notifymyandroid.com/publicapi/verify';
15
+
16
+    /**
17
+     * @const holds the notify url
18
+     */
19
+    const LIB_NMA_NOTIFY = 'https://www.notifymyandroid.com/publicapi/notify';
20
+
21
+    /**
22
+     * toggles on debugging
23
+     *
24
+     * @var bool
25
+     */
26
+    public $debug = false;
27
+
28
+    public $apiCallsRemaining = false;
29
+
30
+    public $apiLimitReset = false;
31
+
32
+    public $lastStatus = false;
33
+    /**
34
+     * @var bool|string
35
+     */
36
+    protected $apiKey = false;
37
+
38
+    /**
39
+     * @var bool|string
40
+     */
41
+    protected $devKey = false;
42
+
43
+
44
+    protected $error_codes
45
+        = array(
46
+            200     => 'Notification submitted.',
47
+            400     => 'The data supplied is in the wrong format, invalid length or null.',
48
+            401     => 'None of the API keys provided were valid.',
49
+            402     => 'Maximum number of API calls per hour exceeded.',
50
+            500     => 'Internal server error. Please contact our support if the problem persists.'
51
+        );
52
+
53
+    /**
54
+     * @param array $options
55
+     */
56
+    function __construct($options = array())
57
+    {
58
+        if (!isset($options['apikey'])) {
59
+            return $this->error('You must supply a API Key');
60
+        } else {
61
+            $this->apiKey = $options['apikey'];
62
+        }
63
+
64
+        if (isset($options['developerkey'])) {
65
+            $this->devKey = $options['developerkey'];
66
+        }
67
+
68
+        if (isset($options['debug'])) {
69
+            $this->debug = true;
70
+        }
71
+
72
+        return true; // this shuts my ide up
73
+
74
+    }
75
+
76
+
77
+    /**
78
+     * @param bool $key [optional] if not set the one used on construct is used
79
+     *
80
+     * @return bool|mixed|SimpleXMLElement|string
81
+     */
82
+    public function verify($key = false)
83
+    {
84
+
85
+        $options = array();
86
+
87
+        if ($key !== false) {
88
+            $options['apikey'] = $key;
89
+        } else {
90
+            $options['apikey'] = $this->apiKey;
91
+        }
92
+
93
+
94
+        if ($this->devKey) {
95
+            $options['developerkey'] = $this->devKey;
96
+        }
97
+
98
+        return $this->makeApiCall(self::LIB_NMA_VERIFY, $options);
99
+    }
100
+
101
+    /**
102
+     * @param string $application
103
+     * @param string $event
104
+     * @param string $description
105
+     * @param string $url
106
+     * @param int    $priority
107
+     * @param bool   $apiKeys
108
+     *
109
+     * @return bool|mixed|SimpleXMLElement|string
110
+     */
111
+    public function notify($application = '', $event = '', $description = '', $url = '', $priority = 0, $apiKeys = false)
112
+    {
113
+        if (empty($application) || empty($event) || empty($description)) {
114
+            return $this->error('you must supply a application name, event and long desc');
115
+        }
116
+
117
+        $post = array('application' => substr($application, 0, 256),
118
+                      'event'       => substr($event, 0, 1000),
119
+                      'description' => substr($description, 0, 10000),
120
+                      'priority'    => $priority
121
+        );
122
+		if (!empty($url)) {
123
+			$post['url'] = substr($url, 0, 2000);
124
+		}
125
+        if ($this->devKey) {
126
+            $post['developerkey'] = $this->devKey;
127
+        }
128
+
129
+        if ($apiKeys !== false) {
130
+            $post['apikey'] = $apiKeys;
131
+        } else {
132
+            $post['apikey'] = $this->apiKey;
133
+        }
134
+
135
+        return $this->makeApiCall(self::LIB_NMA_NOTIFY, $post, 'POST');
136
+    }
137
+
138
+
139
+    /**
140
+     * @param        $url
141
+     * @param null   $params
142
+     * @param string $verb
143
+     * @param string $format
144
+     *
145
+     * @return bool|mixed|SimpleXMLElement|string
146
+     * @throws Exception
147
+     */
148
+    protected function makeApiCall($url, $params = null, $verb = 'GET', $format = 'xml')
149
+    {
150
+        $cparams = array(
151
+            'http' => array(
152
+                'method'        => $verb,
153
+                'ignore_errors' => true
154
+            )
155
+        );
156
+        if ($params !== null && !empty($params)) {
157
+            $params = http_build_query($params);
158
+            if ($verb == 'POST') {
159
+                $cparams["http"]['header'] = 'Content-Type: application/x-www-form-urlencoded';
160
+                $cparams['http']['content'] = $params;
161
+            } else {
162
+                $url .= '?' . $params;
163
+            }
164
+        } else {
165
+            return $this->error(
166
+                'this api requires all calls to have params' . $this->debug ? ', you provided: ' . var_dump($params)
167
+                    : ''
168
+            );
169
+        }
170
+
171
+        $context = stream_context_create($cparams);
172
+        $fp = fopen($url, 'rb', false, $context);
173
+        if (!$fp) {
174
+            $res = false;
175
+        } else {
176
+
177
+            if ($this->debug) {
178
+                $meta = stream_get_meta_data($fp);
179
+                $this->error('var dump of http headers' . var_dump($meta['wrapper_data']));
180
+            }
181
+
182
+            $res = stream_get_contents($fp);
183
+        }
184
+
185
+        if ($res === false) {
186
+            return $this->error("$verb $url failed: $php_errormsg");
187
+        }
188
+
189
+        switch ($format) {
190
+            case 'json':
191
+                return $this->error('this api does not support json');
192
+            /*
193
+            * uncomment the below if json is added later
194
+            * $r = json_decode($res);
195
+           if ($r === null) {
196
+               return $this->error("failed to decode $res as json");
197
+           }
198
+           return $r;*/
199
+
200
+            case 'xml':
201
+                $r = simplexml_load_string($res);
202
+                if ($r === null) {
203
+                    return $this->error("failed to decode $res as xml");
204
+                }
205
+                return $this->process_xml_return($r);
206
+        }
207
+        return $res;
208
+    }
209
+
210
+    /**
211
+     * @param     $message
212
+     * @param int $type
213
+     *
214
+     * @return bool
215
+     * @throws Exception
216
+     */
217
+    private function error($message, $type = E_USER_NOTICE)
218
+    {
219
+        if (self::LIB_ERROR_TYPE == 'error') {
220
+            trigger_error($message, $type);
221
+            return false;
222
+        } else {
223
+            throw new Exception($message, $type);
224
+        }
225
+    }
226
+
227
+    /**
228
+     * @param SimpleXMLElement $obj
229
+     *
230
+     * @return bool
231
+     */
232
+    private function process_xml_return(SimpleXMLElement $obj)
233
+    {
234
+
235
+        if (isset($obj->success)) {
236
+            $this->lastStatus = $obj->success["@attributes"]['code'];
237
+
238
+            $this->apiCallsRemaining = $obj->success["@attributes"]['remaining'];
239
+            $this->apiLimitReset = $obj->success["@attributes"]['resettimer'];
240
+            return true;
241
+        } elseif (isset($obj->error)) {
242
+            if (isset($obj->error["@attributes"])) {
243
+                $this->lastStatus = $obj->error["@attributes"]['code'];
244
+
245
+                if (isset($obj->error["@attributes"]['resettimer'])) {
246
+                    $this->apiLimitReset = $obj->error["@attributes"]['resettimer'];
247
+                }
248
+
249
+            }
250
+            return $this->error($obj->error);
251
+        } else {
252
+            return $this->error("unkown error");
253
+        }
254
+    }
255
+
256
+}

+ 3
- 0
classes/artist.class.php View File

1
+<?
2
+// Placeholder for if we ever decide to actaully have a model for an artist.
3
+?>

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

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

+ 385
- 0
classes/artists_similar.class.php View File

1
+<?
2
+class ARTIST {
3
+	var $ID = 0;
4
+	var $Name = 0;
5
+	var $NameLength = 0;
6
+	var $SimilarID = 0;
7
+	var $Displayed = false;
8
+	var $x = 0;
9
+	var $y = 0;
10
+	var $Similar = array();
11
+
12
+	function ARTIST($ID = '', $Name = '') {
13
+		$this->ID = $ID;
14
+		$this->NameLength = mb_strlen($Name, 'utf8');
15
+		$this->Name = display_str($Name);
16
+	}
17
+}
18
+
19
+class ARTISTS_SIMILAR extends ARTIST{
20
+	var $Artists = array();
21
+	var $TotalScore = 0;
22
+
23
+	var $xValues = array(WIDTH=>1);
24
+	var $yValues = array(HEIGHT=>1);
25
+
26
+	var $LargestDecimal = 0;
27
+	var $LowestDecimal = 1;
28
+
29
+
30
+
31
+	function dump_data() {
32
+		return serialize(array(time(), $this->Name, $this->x, $this->y, serialize($this->Artists), serialize($this->Similar)));
33
+	}
34
+
35
+	function load_data($Data) {
36
+		list($LastUpdated, $this->Name, $this->x, $this->y, $this->Artists, $this->Similar) = unserialize($Data);
37
+		$this->Artists = unserialize($this->Artists);
38
+		$this->Similar = unserialize($this->Similar);
39
+	}
40
+
41
+	function set_up() {
42
+		$QueryID = G::$DB->get_query_id();
43
+
44
+		$this->x = ceil(WIDTH / 2);
45
+		$this->y = ceil(HEIGHT / 2);
46
+
47
+		$this->xValues[$this->x] = $this->ID;
48
+		$this->yValues[$this->y] = $this->ID;
49
+
50
+
51
+		// Get artists that are directly similar to the artist
52
+		$ArtistIDs = array();
53
+		G::$DB->query("
54
+			SELECT
55
+				s2.ArtistID,
56
+				ag.Name,
57
+				ass.Score
58
+			FROM artists_similar AS s1
59
+				JOIN artists_similar AS s2 ON s1.SimilarID=s2.SimilarID AND s1.ArtistID!=s2.ArtistID
60
+				JOIN artists_similar_scores AS ass ON ass.SimilarID=s1.SimilarID
61
+				JOIN artists_group AS ag ON ag.ArtistID=s2.ArtistID
62
+			WHERE s1.ArtistID=".$this->ID."
63
+			ORDER BY ass.Score DESC
64
+			LIMIT 14");
65
+
66
+		if (!G::$DB->has_results()) {
67
+			return;
68
+		}
69
+
70
+		// Build into array. Each artist is its own object in $this->Artists
71
+		while (list($ArtistID, $Name, $Score) = G::$DB->next_record(MYSQLI_NUM, false)) {
72
+			if ($Score < 0) {
73
+				continue;
74
+			}
75
+			$this->Artists[$ArtistID] = new ARTIST($ArtistID, $Name);
76
+			$this->Similar[$ArtistID] = array('ID' => $ArtistID, 'Score' => $Score);
77
+			$this->TotalScore += $Score;
78
+			$ArtistIDs[] = $ArtistID;
79
+		}
80
+
81
+		// Get similarities between artists on the map
82
+		G::$DB->query("
83
+			SELECT
84
+				s1.ArtistID,
85
+				s2.ArtistID
86
+			FROM artists_similar AS s1
87
+				JOIN artists_similar AS s2 ON s1.SimilarID=s2.SimilarID AND s1.ArtistID!=s2.ArtistID
88
+				JOIN artists_similar_scores AS ass ON ass.SimilarID=s1.SimilarID
89
+				JOIN artists_group AS a ON a.ArtistID=s2.ArtistID
90
+			WHERE s1.ArtistID IN(".implode(',', $ArtistIDs).')
91
+				AND s2.ArtistID IN('.implode(',', $ArtistIDs).')');
92
+
93
+		// Build into array
94
+		while (list($Artist1ID, $Artist2ID) = G::$DB->next_record()) {
95
+			$this->Artists[$Artist1ID]->Similar[$Artist2ID] = array('ID'=>$Artist2ID);
96
+		}
97
+
98
+		// Calculate decimal point scores between artists
99
+		foreach ($this->Similar as $SimilarArtist) {
100
+			list($ArtistID, $Similar) = array_values($SimilarArtist);
101
+			$this->Similar[$ArtistID]['Decimal'] =  $this->similarity($Similar['Score'], $this->TotalScore);
102
+
103
+			if ($this->Similar[$ArtistID]['Decimal'] < $this->LowestDecimal) {
104
+				$this->LowestDecimal = $this->Similar[$ArtistID]['Decimal'];
105
+			}
106
+			if ($this->Similar[$ArtistID]['Decimal'] > $this->LargestDecimal) {
107
+				$this->LargestDecimal = $this->Similar[$ArtistID]['Decimal'];
108
+			}
109
+		}
110
+		reset($this->Artists);
111
+
112
+		G::$DB->set_query_id($QueryID);
113
+	}
114
+
115
+	function set_positions() {
116
+		$xValues = array(); // Possible x values
117
+		$Root = ceil(WIDTH / 4); // Half-way into half of the image
118
+		$Offset = 4; // Distance from the root (a quarter of the way into the image) to the x value
119
+
120
+		// The number of artists placed in the top or the bottom
121
+		$NumTop = 0;
122
+		$NumBottom = 0;
123
+
124
+		// The number of artists placed in the left or the right
125
+		$NumLeft = 0;
126
+		$NumRight = 0;
127
+
128
+		$Multiplier = 0;
129
+
130
+		// Build up an impressive list of possible x values
131
+		// We later iterate through these, and pick out the ones we want
132
+
133
+		// These x values are all below WIDTH/2 (all on the left)
134
+		// The script later chooses which side to put them on
135
+
136
+		// We create more very low x values because they're more likely to be skipped
137
+		for ($i = 0; $i <= count($this->Artists) * 4; $i++) {
138
+			if ($Offset >= ((WIDTH / 4))) {
139
+				$Offset = $Offset % (WIDTH / 4);
140
+			}
141
+			$Plus = $Root + $Offset; // Point on the right of the root
142
+			$Minus = abs($Root - $Offset); // Point on the left of the root
143
+
144
+			$xValues[$Plus] = $Plus;
145
+
146
+			$xValues[$Minus] = $Minus;
147
+
148
+			// Throw in an extra x value closer to the edge, because they're more likely to be skipped
149
+
150
+			if ($Minus > 30) {
151
+			//	$xValues[$Minus - 30] = $Minus - 30;
152
+			}
153
+
154
+			$Offset = $Offset + rand(5, 20); // Increase offset, and go again
155
+		}
156
+
157
+		foreach ($this->Artists as $Artist) {
158
+			$ArtistID = $Artist->ID;
159
+			if ($Artist->Displayed == true) {
160
+				continue;
161
+			}
162
+			$this->Similar[$ArtistID]['Decimal'] = $this->Similar[$ArtistID]['Decimal'] * (1 / ($this->LargestDecimal)) - 0.1;
163
+			// Calculate the distance away from the center, based on similarity
164
+			$IdealDistance =  $this->calculate_distance($this->Similar[$ArtistID]['Decimal'], $this->x, $this->y);
165
+
166
+			$this->Similar[$ArtistID]['Distance'] = $IdealDistance;
167
+
168
+			// 1 = left, 2 = right
169
+			$Horizontal = 0;
170
+			$Vertical = 0;
171
+
172
+			// See if any similar artists have been placed yet. If so, place artist in that half
173
+			// (provided that there are enough in the other half to visually balance out)
174
+			reset($Artist->Similar);
175
+			foreach ($Artist->Similar as $SimilarArtist) {
176
+				list($Artist2ID) = array_values($SimilarArtist);
177
+				if ($this->Artists[$Artist2ID]) {
178
+					if ($this->Artists[$Artist2ID]->x > (WIDTH / 2) && ($NumRight-$NumLeft) < 1) {
179
+						$Horizontal = 2;
180
+					} elseif ($NumLeft - $NumRight < 1) {
181
+						$Horizontal = 1;
182
+					}
183
+					break;
184
+				}
185
+			}
186
+
187
+			shuffle($xValues);
188
+
189
+			while ($xValue = array_shift($xValues)) {
190
+				if (abs($this->x - $xValue) <= $IdealDistance) {
191
+					if (hypot(abs($this->x - $xValue), ($this->y - 50)) > $IdealDistance
192
+						|| ceil(sqrt(pow($IdealDistance, 2) - pow($this->x - $xValue, 2))) > (HEIGHT / 2)) {
193
+						$xValue = $this->x - ceil(sqrt(pow($IdealDistance, 2) - pow($IdealDistance * 0.1 * rand(5,9), 2)));
194
+						//echo "Had to change x value for ".$Artist->Name." to ".$xValue."\n";
195
+					}
196
+					// Found a match (Is close enough to the center to satisfy $IdealDistance),
197
+					// Now it's time to choose which half to put it on
198
+					if (!$Horizontal) {
199
+						// No similar artists displayed
200
+						$Horizontal = ($NumLeft < $NumRight) ? 1 : 2;
201
+					}
202
+					if ($Horizontal == 2) {
203
+						$xValue = WIDTH - $xValue;
204
+						$NumRight++;
205
+					} else {
206
+						$NumLeft++;
207
+					}
208
+
209
+					$Artist->x = $xValue;
210
+					$this->xValues[$xValue] = $ArtistID;
211
+					unset($xValues[$xValue]);
212
+
213
+					break;
214
+				}
215
+			}
216
+			if (!$xValue) { // Uh-oh, we were unable to choose an x value.
217
+				$xValue = ceil(sqrt(pow($IdealDistance, 2) / 2));
218
+				$xValue = (WIDTH / 2) - $xValue;
219
+				$Artist->x = $xValue;
220
+				$this->xValues[$xValue] = $ArtistID;
221
+				unset($xValues[$xValue]);
222
+			}
223
+
224
+
225
+			// Pythagoras. $yValue is the vertical distance from the center to the y value
226
+			$yValue = sqrt(pow($IdealDistance, 2) - pow(abs($this->x - $Artist->x), 2));
227
+
228
+
229
+			// Now we pick if it should go on the top or bottom
230
+
231
+			if ($NumTop > $NumBottom) { // Send it to the bottom half
232
+				$yValue = (HEIGHT / 2) + $yValue;
233
+				$NumBottom++;
234
+			} else {
235
+				$yValue=(HEIGHT / 2) - $yValue;
236
+				$NumTop++;
237
+			}
238
+
239
+			$yValue = ceil($yValue);
240
+
241
+			// $yValue is now a proper y coordinate
242
+			// Now time to do some spacing out
243
+
244
+			if ($yValue < 10) {
245
+				$yValue += (10 + abs($yValue)) + rand(10,20);
246
+			}
247
+
248
+			if ($yValue > (HEIGHT - 10)) {
249
+				$yValue -= ((HEIGHT / 2) - rand(10,20));
250
+			}
251
+
252
+			$i = 1;
253
+			while ($Conflict = $this->scan_array_range($this->yValues, abs($yValue - 13), $yValue + 13)) {
254
+				if ($i > 10) {
255
+					break;
256
+				}
257
+				if (!$this->scan_array_range($this->yValues, abs($yValue - 5), $yValue - 20)) {
258
+					$yValue -= 20;
259
+				}
260
+
261
+				$yValue = $Conflict + rand(10, 20);
262
+				if ($yValue > HEIGHT - 10) {
263
+					$yValue -= ceil(HEIGHT / 2.5);
264
+				} elseif ($yValue < 10) {
265
+					$yValue += ceil(HEIGHT / 2.5);
266
+				}
267
+				$i++;
268
+			}
269
+
270
+			$Artist->y = $yValue;
271
+			$this->yValues[$yValue] = $ArtistID;
272
+		}
273
+		reset($this->Artists);
274
+		reset($this->xValues);
275
+		reset($this->yValues);
276
+
277
+	}
278
+
279
+	// Calculate the ideal distance from the center point ($Rootx, $Rooty) to the artist's point on the board
280
+	// Pythagoras as fun!
281
+	function calculate_distance($SimilarityCoefficient, $Rootx, $Rooty) {
282
+		$MaxWidth = WIDTH - $Rootx;
283
+		$MaxHeight = HEIGHT - $Rooty;
284
+		$x = $MaxWidth - ($SimilarityCoefficient * $MaxWidth * 0.01); // Possible x value
285
+		$y = $MaxHeight - ($SimilarityCoefficient * $MaxHeight); // Possible y value
286
+		$Hypot = hypot($Rootx - $x, $Rooty - $y);
287
+		return $MaxWidth - $Hypot;
288
+
289
+	}
290
+
291
+	function similarity($Score, $TotalArtistScore) {
292
+		return (pow(($Score / ($TotalArtistScore + 1)), (1 / 1)));
293
+	}
294
+
295
+	function scan_array_range($Array, $Start, $Finish) {
296
+		if ($Start < 0) {
297
+			die($Start);
298
+		}
299
+		for ($i = $Start; $i <= $Finish; $i++) {
300
+			if (isset($Array[$i])) {
301
+				return $i;
302
+			}
303
+		}
304
+		return false;
305
+	}
306
+
307
+	function write_artists() {
308
+?>
309
+		<div style="position: absolute; bottom: <?=($this->y - 10)?>px; left: <?=($this->x - $this->NameLength * 4)?>px; font-size: 13pt; white-space: nowrap;" class="similar_artist_header">
310
+			<?=($this->Name)?>
311
+		</div>
312
+<?
313
+		foreach ($this->Artists as $Artist) {
314
+			if ($Artist->ID == $this->ID) {
315
+				continue;
316
+			}
317
+			$xPosition = $Artist->x - $Artist->NameLength * 4;
318
+			if ($xPosition < 0) {
319
+				$xPosition = 3;
320
+				$Artist->x = $xPosition;
321
+
322
+			}
323
+			$Decimal = $this->Similar[$Artist->ID]['Decimal'];
324
+
325
+			if ($Decimal < 0.2) {
326
+				$FontSize = 8;
327
+			} elseif ($Decimal < 0.3) {
328
+				$FontSize = 9;
329
+			} elseif ($Decimal < 0.4) {
330
+				$FontSize = 10;
331
+			} else {
332
+				$FontSize = 12;
333
+			}
334
+?>
335
+		<div style="position: absolute; top: <?=($Artist->y - 5)?>px; left: <?=$xPosition?>px; font-size: <?=$FontSize?>pt; white-space: nowrap;">
336
+			<a href="artist.php?id=<?=($Artist->ID)?>" class="similar_artist"><?=($Artist->Name)?></a>
337
+		</div>
338
+<?
339
+		}
340
+		reset($this->Artists);
341
+	}
342
+
343
+	function background_image() {
344
+		global $Img;
345
+		reset($this->Similar);
346
+		foreach ($this->Similar as $SimilarArtist) {
347
+			list($ArtistID, $Val) = array_values($SimilarArtist);
348
+			$Artist = $this->Artists[$ArtistID];
349
+			$Decimal = $this->Similar[$ArtistID]['Decimal'];
350
+			$Width = ceil($Decimal * 4) + 1;
351
+
352
+			$Img->line($this->x, $this->y, $Artist->x, $Artist->y, $Img->color(199, 218, 255), $Width);
353
+
354
+			unset($Artist->Similar[$this->ID]);
355
+			reset($Artist->Similar);
356
+			foreach ($Artist->Similar as $SimilarArtist2) {
357
+				list($Artist2ID) = array_values($SimilarArtist2);
358
+				if ($this->Artists[$Artist2ID]) {
359
+					$Artist2 = $this->Artists[$Artist2ID];
360
+					$Img->line($Artist->x, $Artist->y, $Artist2->x, $Artist2->y, $Img->color(173, 201, 255));
361
+					unset($Artist2->Similar[$ArtistID]);
362
+				}
363
+			}
364
+			reset($this->xValues);
365
+		}
366
+		$Img->make_png(SERVER_ROOT.'/static/similar/'.$this->ID.'.png');
367
+	}
368
+
369
+	function dump() {
370
+		echo "Similarities:\n";
371
+		foreach ($this->Artists as $Artist) {
372
+			echo $Artist->ID;
373
+			echo ' - ';
374
+			echo $Artist->Name;
375
+			echo "\n";
376
+			echo 'x - ' . $Artist->x . "\n";
377
+			echo 'y - ' . $Artist->y . "\n";
378
+			print_r($this->Similar[$Artist->ID]);
379
+			//print_r($Artist->Similar);
380
+			echo "\n\n---\n\n";
381
+		}
382
+
383
+	}
384
+}
385
+?>

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

1
+<?
2
+
3
+class AutoEnable {
4
+
5
+    // Constants for database values
6
+    const APPROVED = 1;
7
+    const DENIED = 2;
8
+    const DISCARDED = 3;
9
+
10
+    // Cache key to store the number of enable requests
11
+    const CACHE_KEY_NAME = 'num_enable_requests';
12
+
13
+    // The default request rejected message
14
+    const REJECTED_MESSAGE = "Your request to re-enable your account has been rejected.<br />This may be because a request is already pending for your username, or because a recent request was denied.<br /><br />You are encouraged to discuss this with staff by visiting %s on %s";
15
+
16
+    // The default request received message
17
+    const RECEIVED_MESSAGE = "Your request to re-enable your account has been received. You can expect a reply message in your email within 48 hours.<br />If you do not receive an email after 48 hours have passed, please visit us on IRC for assistance.";
18
+
19
+    /**
20
+     * Handle a new enable request
21
+     *
22
+     * @param string $Username The user's username
23
+     * @param string $Email The user's email address
24
+     * @return string The output
25
+     */
26
+    public static function new_request($Username, $Email) {
27
+        if (empty($Username)) {
28
+            header("Location: login.php");
29
+            die();
30
+        }
31
+
32
+        // Get the user's ID
33
+        G::$DB->query("
34
+                SELECT um.ID
35
+                FROM users_main AS um
36
+                JOIN users_info ui ON ui.UserID = um.ID
37
+                WHERE um.Username = '$Username'
38
+                  AND um.Enabled = '2'");
39
+
40
+        if (G::$DB->has_results()) {
41
+            // Make sure the user can make another request
42
+            list($UserID) = G::$DB->next_record();
43
+            G::$DB->query("
44
+            SELECT 1 FROM users_enable_requests
45
+            WHERE UserID = '$UserID'
46
+              AND (
47
+                    (
48
+                      Timestamp > NOW() - INTERVAL 1 WEEK
49
+                        AND HandledTimestamp IS NULL
50
+                    )
51
+                    OR
52
+                    (
53
+                      Timestamp > NOW() - INTERVAL 2 MONTH
54
+                        AND
55
+                          (Outcome = '".self::DENIED."'
56
+                             OR Outcome = '".self::DISCARDED."')
57
+                    )
58
+                  )");
59
+        }
60
+
61
+        $IP = $_SERVER['REMOTE_ADDR'];
62
+
63
+        if (G::$DB->has_results() || !isset($UserID)) {
64
+            // User already has/had a pending activation request or username is invalid
65
+            $Output = sprintf(self::REJECTED_MESSAGE, BOT_DISABLED_CHAN, BOT_SERVER);
66
+            if (isset($UserID)) {
67
+                Tools::update_user_notes($UserID, sqltime() . " - Enable request rejected from $IP\n\n");
68
+            }
69
+        } else {
70
+            // New disable activation request
71
+            $UserAgent = db_string($_SERVER['HTTP_USER_AGENT']);
72
+
73
+            G::$DB->query("
74
+                INSERT INTO users_enable_requests
75
+                (UserID, Email, IP, UserAgent, Timestamp)
76
+                VALUES ('$UserID', '".DBCrypt::encrypt($Email)."', '".DBCrypt::encrypt($IP)."', '$UserAgent', '".sqltime()."')");
77
+
78
+            // Cache the number of requests for the modbar
79
+            G::$Cache->increment_value(self::CACHE_KEY_NAME);
80
+            setcookie('username', '', time() - 60 * 60, '/', '', false);
81
+            $Output = self::RECEIVED_MESSAGE;
82
+            Tools::update_user_notes($UserID, sqltime() . " - Enable request " . G::$DB->inserted_id() . " received from $IP\n\n");
83
+        }
84
+
85
+        return $Output;
86
+    }
87
+
88
+    /*
89
+     * Handle requests
90
+     *
91
+     * @param int|int[] $IDs An array of IDs, or a single ID
92
+     * @param int $Status The status to mark the requests as
93
+     * @param string $Comment The staff member comment
94
+     */
95
+    public static function handle_requests($IDs, $Status, $Comment) {
96
+        if ($Status != self::APPROVED && $Status != self::DENIED && $Status != self::DISCARDED) {
97
+            error(404);
98
+        }
99
+
100
+        $UserInfo = array();
101
+        $IDs = (!is_array($IDs)) ? [$IDs] : $IDs;
102
+
103
+        if (count($IDs) == 0) {
104
+            error(404);
105
+        }
106
+
107
+        foreach ($IDs as $ID) {
108
+            if (!is_number($ID)) {
109
+                error(404);
110
+            }
111
+        }
112
+
113
+        G::$DB->query("SELECT Email, ID, UserID
114
+                FROM users_enable_requests
115
+                WHERE ID IN (".implode(',', $IDs).")
116
+                    AND Outcome IS NULL");
117
+        $Results = G::$DB->to_array(false, MYSQLI_NUM);
118
+
119
+        if ($Status != self::DISCARDED) {
120
+            // Prepare email
121
+            require(SERVER_ROOT . '/classes/templates.class.php');
122
+            $TPL = NEW TEMPLATE;
123
+            if ($Status == self::APPROVED) {
124
+                $TPL->open(SERVER_ROOT . '/templates/enable_request_accepted.tpl');
125
+                $TPL->set('SITE_URL', NONSSL_SITE_URL);
126
+            } else {
127
+                $TPL->open(SERVER_ROOT . '/templates/enable_request_denied.tpl');
128
+            }
129
+
130
+            $TPL->set('SITE_NAME', SITE_NAME);
131
+
132
+            foreach ($Results as $Result) {
133
+                list($Email, $ID, $UserID) = $Result;
134
+                $Email = DBCrypt::decrypt($Email);
135
+                $UserInfo[] = array($ID, $UserID);
136
+
137
+                if ($Status == self::APPROVED) {
138
+                    // Generate token
139
+                    $Token = db_string(Users::make_secret());
140
+                    G::$DB->query("
141
+                        UPDATE users_enable_requests
142
+                        SET Token = '$Token'
143
+                        WHERE ID = '$ID'");
144
+                    $TPL->set('TOKEN', $Token);
145
+                }
146
+
147
+                // Send email
148
+                $Subject = "Your enable request for " . SITE_NAME . " has been ";
149
+                $Subject .= ($Status == self::APPROVED) ? 'approved' : 'denied';
150
+
151
+                Misc::send_email($Email, $Subject, $TPL->get(), 'noreply');
152
+            }
153
+        } else {
154
+            foreach ($Results as $Result) {
155
+                list(, $ID, $UserID) = $Result;
156
+                $UserInfo[] = array($ID, $UserID);
157
+            }
158
+        }
159
+
160
+        // User notes stuff
161
+        G::$DB->query("
162
+            SELECT Username
163
+            FROM users_main
164
+            WHERE ID = '" . G::$LoggedUser['ID'] . "'");
165
+        list($StaffUser) = G::$DB->next_record();
166
+
167
+        foreach ($UserInfo as $User) {
168
+            list($ID, $UserID) = $User;
169
+            $BaseComment = sqltime() . " - Enable request $ID " . strtolower(self::get_outcome_string($Status)) . ' by [user]'.$StaffUser.'[/user]';
170
+            $BaseComment .= (!empty($Comment)) ? "\nReason: $Comment\n\n" : "\n\n";
171
+            Tools::update_user_notes($UserID, $BaseComment);
172
+        }
173
+
174
+        // Update database values and decrement cache
175
+        G::$DB->query("
176
+                UPDATE users_enable_requests
177
+                SET HandledTimestamp = '".sqltime()."',
178
+                    CheckedBy = '".G::$LoggedUser['ID']."',
179
+                    Outcome = '$Status'
180
+                WHERE ID IN (".implode(',', $IDs).")");
181
+        G::$Cache->decrement_value(self::CACHE_KEY_NAME, count($IDs));
182
+    }
183
+
184
+    /**
185
+     * Unresolve a discarded request
186
+     *
187
+     * @param int $ID The request ID
188
+     */
189
+    public static function unresolve_request($ID) {
190
+        $ID = (int) $ID;
191
+
192
+        if (empty($ID)) {
193
+            error(404);
194
+        }
195
+
196
+        G::$DB->query("
197
+            SELECT UserID
198
+            FROM users_enable_requests
199
+            WHERE Outcome = '" . self::DISCARDED . "'
200
+              AND ID = '$ID'");
201
+
202
+        if (!G::$DB->has_results()) {
203
+            error(404);
204
+        } else {
205
+            list($UserID) = G::$DB->next_record();
206
+        }
207
+
208
+        G::$DB->query("
209
+            SELECT Username
210
+            FROM users_main
211
+            WHERE ID = '" . G::$LoggedUser['ID'] . "'");
212
+        list($StaffUser) = G::$DB->next_record();
213
+
214
+        Tools::update_user_notes($UserID, sqltime() . " - Enable request $ID unresolved by [user]" . $StaffUser . '[/user]' . "\n\n");
215
+        G::$DB->query("
216
+            UPDATE users_enable_requests
217
+            SET Outcome = NULL, HandledTimestamp = NULL, CheckedBy = NULL
218
+            WHERE ID = '$ID'");
219
+        G::$Cache->increment_value(self::CACHE_KEY_NAME);
220
+    }
221
+
222
+    /**
223
+     * Get the corresponding outcome string for a numerical value
224
+     *
225
+     * @param int $Outcome The outcome integer
226
+     * @return string The formatted output string
227
+     */
228
+    public static function get_outcome_string($Outcome) {
229
+        if ($Outcome == self::APPROVED) {
230
+            $String = "Approved";
231
+        } else if ($Outcome == self::DENIED) {
232
+            $String = "Rejected";
233
+        } else if ($Outcome == self::DISCARDED) {
234
+            $String = "Discarded";
235
+        } else {
236
+            $String = "---";
237
+        }
238
+
239
+        return $String;
240
+    }
241
+
242
+    /**
243
+     * Handle a user's request to enable an account
244
+     *
245
+     * @param string $Token The token
246
+     * @return string The error output, or an empty string
247
+     */
248
+    public static function handle_token($Token) {
249
+        $Token = db_string($Token);
250
+        G::$DB->query("
251
+            SELECT uer.UserID, uer.HandledTimestamp, um.torrent_pass, um.Visible, um.IP
252
+            FROM users_enable_requests AS uer
253
+            LEFT JOIN users_main AS um ON uer.UserID = um.ID
254
+            WHERE Token = '$Token'");
255
+
256
+        if (G::$DB->has_results()) {
257
+            list($UserID, $Timestamp, $TorrentPass, $Visible, $IP) = G::$DB->next_record();
258
+            G::$DB->query("UPDATE users_enable_requests SET Token = NULL WHERE Token = '$Token'");
259
+            if ($Timestamp < time_minus(3600 * 48)) {
260
+                // Old request
261
+                Tools::update_user_notes($UserID, sqltime() . " - Tried to use an expired enable token from ".$_SERVER['REMOTE_ADDR']."\n\n");
262
+                $Err = "Token has expired. Please visit ".BOT_DISABLED_CHAN." on ".BOT_SERVER." to discuss this with staff.";
263
+            } else {
264
+                // Good request, decrement cache value and enable account
265
+                G::$Cache->decrement_value(AutoEnable::CACHE_KEY_NAME);
266
+                $VisibleTrIP = ($Visible && DBCrypt::decrypt($IP) != '127.0.0.1') ? '1' : '0';
267
+                Tracker::update_tracker('add_user', array('id' => $UserID, 'passkey' => $TorrentPass, 'visible' => $VisibleTrIP));
268
+                G::$DB->query("UPDATE users_main SET Enabled = '1', can_leech = '1' WHERE ID = '$UserID'");
269
+                G::$DB->query("UPDATE users_info SET BanReason = '0' WHERE UserID = '$UserID'");
270
+                G::$Cache->delete_value('user_info_'.$UserID);
271
+                $Err = "Your account has been enabled. You may now log in.";
272
+            }
273
+        } else {
274
+            $Err = "Invalid token.";
275
+        }
276
+
277
+        return $Err;
278
+    }
279
+
280
+    /**
281
+     * Build the search query, from the searchbox inputs
282
+     *
283
+     * @param int $UserID The user ID
284
+     * @param string $IP The IP
285
+     * @param string $SubmittedTimestamp The timestamp representing when the request was submitted
286
+     * @param int $HandledUserID The ID of the user that handled the request
287
+     * @param string $HandledTimestamp The timestamp representing when the request was handled
288
+     * @param int $OutcomeSearch The outcome of the request
289
+     * @param boolean $Checked Should checked requests be included?
290
+     * @return array The WHERE conditions for the query
291
+     */
292
+    public static function build_search_query($Username, $IP, $SubmittedBetween, $SubmittedTimestamp1, $SubmittedTimestamp2, $HandledUsername, $HandledBetween, $HandledTimestamp1, $HandledTimestamp2, $OutcomeSearch, $Checked) {
293
+        $Where = array();
294
+
295
+        if (!empty($Username)) {
296
+            $Where[] = "um1.Username = '$Username'";
297
+        }
298
+
299
+        if (!empty($IP)) {
300
+            // TODO: make this work with encrypted IPs
301
+            $Where[] = "uer.IP = '$IP'";
302
+        }
303
+
304
+        if (!empty($SubmittedTimestamp1)) {
305
+            switch($SubmittedBetween) {
306
+                case 'on':
307
+                    $Where[] = "DATE(uer.Timestamp) = DATE('$SubmittedTimestamp1')";
308
+                    break;
309
+                case 'before':
310
+                    $Where[] = "DATE(uer.Timestamp) < DATE('$SubmittedTimestamp1')";
311
+                    break;
312
+                case 'after':
313
+                    $Where[] = "DATE(uer.Timestamp) > DATE('$SubmittedTimestamp1')";
314
+                    break;
315
+                case 'between':
316
+                    if (!empty($SubmittedTimestamp2)) {
317
+                        $Where[] = "DATE(uer.Timestamp) BETWEEN DATE('$SubmittedTimestamp1') AND DATE('$SubmittedTimestamp2')";
318
+                    }
319
+                    break;
320
+                default:
321
+                    break;
322
+            }
323
+        }
324
+
325
+        if (!empty($HandledTimestamp1)) {
326
+            switch($HandledBetween) {
327
+                case 'on':
328
+                    $Where[] = "DATE(uer.HandledTimestamp) = DATE('$HandledTimestamp1')";
329
+                    break;
330
+                case 'before':
331
+                    $Where[] = "DATE(uer.HandledTimestamp) < DATE('$HandledTimestamp1')";
332
+                    break;
333
+                case 'after':
334
+                    $Where[] = "DATE(uer.HandledTimestamp) > DATE('$HandledTimestamp1')";
335
+                    break;
336
+                case 'between':
337
+                    if (!empty($HandledTimestamp2)) {
338
+                        $Where[] = "DATE(uer.HandledTimestamp) BETWEEN DATE('$HandledTimestamp1') AND DATE('$HandledTimestamp2')";
339
+                    }
340
+                    break;
341
+                default:
342
+                    break;
343
+            }
344
+        }
345
+
346
+        if (!empty($HandledUsername)) {
347
+            $Where[] = "um2.Username = '$HandledUsername'";
348
+        }
349
+
350
+        if (!empty($OutcomeSearch)) {
351
+            $Where[] = "uer.Outcome = '$OutcomeSearch'";
352
+        }
353
+
354
+        if ($Checked) {
355
+            // This is to skip the if statement in enable_requests.php
356
+            $Where[] = "(uer.Outcome IS NULL OR uer.Outcome IS NOT NULL)";
357
+        }
358
+
359
+        return $Where;
360
+    }
361
+}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 140
- 0
classes/calendar.class.php View File

1
+<?
2
+class Calendar {
3
+	public static $Categories = array(1 => "IRC Meeting", "IRC Brainstorm", "Poll Deadline", "Feature Release", "Blog Post", "Announcement", "Featured Album", "Product Release", "Staff Picks", "Forum Brainstorm", "Forum Discussion", "Promotion", "Absence", "Task");
4
+	public static $Importances = array(1 => "Critical", "Important", "Average", "Meh");
5
+	public static $Colors = array(
6
+									"Critical" => "red",
7
+									"Important" => "yellow",
8
+									"Average" => "green",
9
+									"Meh" => "blue");
10
+
11
+	public static $Teams = array(
12
+									0 => "Everyone",
13
+									1 => "Staff"
14
+									
15
+									);
16
+
17
+	public static function can_view() {
18
+		return check_perms('users_mod')
19
+			
20
+			;
21
+	}
22
+
23
+	private static function get_teams_query() {
24
+		$Teams = array(0);
25
+		$IsMod = check_perms("users_mod");
26
+		if ($IsMod) {
27
+			$Teams[] = 1;
28
+		}
29
+		
30
+		return "Team IN (" . implode(",", $Teams) . ") ";
31
+	}
32
+
33
+	public static function get_events($Month, $Year) {
34
+		if (empty($Month) || empty($Year)) {
35
+			$Date = getdate();
36
+			$Month = $Date['mon'];
37
+			$Year = $Date['year'];
38
+		}
39
+		$Month = (int)$Month;
40
+		$Year = (int)$Year;
41
+
42
+		$TeamsSQL = self::get_teams_query();
43
+
44
+		$QueryID = G::$DB->get_query_id();
45
+		G::$DB->query("
46
+						SELECT
47
+							ID, Team, Title, Category, Importance, DAY(StartDate) AS StartDay, DAY(EndDate) AS EndDay
48
+						FROM calendar
49
+						WHERE
50
+							MONTH(StartDate) = '$Month'
51
+						AND
52
+							YEAR(StartDate) = '$Year'
53
+						AND
54
+							$TeamsSQL");
55
+		$Events = G::$DB->to_array();
56
+		G::$DB->set_query_id($QueryID);
57
+		return $Events;
58
+	}
59
+
60
+	public static function get_event($ID) {
61
+		$ID = (int)$ID;
62
+		if (empty($ID)) {
63
+			error("Invalid ID");
64
+		}
65
+		$TeamsSQL = self::get_teams_query();
66
+		$QueryID = G::$DB->get_query_id();
67
+		G::$DB->query("
68
+						SELECT
69
+							ID, Team, Title, Body, Category, Importance, AddedBy, StartDate, EndDate
70
+						FROM calendar
71
+						WHERE
72
+							ID = '$ID'
73
+						AND
74
+							$TeamsSQL");
75
+		$Event = G::$DB->next_record(MYSQLI_ASSOC);
76
+		G::$DB->set_query_id($QueryID);
77
+		return $Event;
78
+	}
79
+
80
+	public static function create_event($Title, $Body, $Category, $Importance, $Team, $UserID, $StartDate, $EndDate = null) {
81
+		if (empty($Title) || empty($Body) || !is_number($Category) || !is_number($Importance)  || !is_number($Team) || empty($StartDate)) {
82
+			error("Error adding event");
83
+		}
84
+		$Title = db_string($Title);
85
+		$Body = db_string($Body);
86
+		$Category = (int)$Category;
87
+		$Importance = (int)$Importance;
88
+		$UserID = (int)$UserID;
89
+		$Team = (int)$Team;
90
+		$StartDate = db_string($StartDate);
91
+		$EndDate = db_string($EndDate);
92
+
93
+		$QueryID = G::$DB->get_query_id();
94
+		G::$DB->query("
95
+						INSERT INTO calendar
96
+							(Title, Body, Category, Importance, Team, StartDate, EndDate, AddedBy)
97
+						VALUES
98
+							('$Title', '$Body', '$Category', '$Importance', '$Team', '$StartDate', '$EndDate', '$UserID')");
99
+		G::$DB->set_query_id($QueryID);
100
+		send_irc("PRIVMSG " . ADMIN_CHAN . " :!mod New calendar event created! Event: $Title; Starts: $StartDate; Ends: $EndDate.");
101
+	}
102
+
103
+	public static function update_event($ID, $Title, $Body, $Category, $Importance, $Team, $StartDate, $EndDate = null) {
104
+		if (!is_number($ID) || empty($Title) || empty($Body) || !is_number($Category) || !is_number($Importance) || !is_number($Team) || empty($StartDate)) {
105
+			error("Error updating event");
106
+		}
107
+		$ID = (int)$ID;
108
+		$Title = db_string($Title);
109
+		$Body = db_string($Body);
110
+		$Category = (int)$Category;
111
+		$Importance = (int)$Importance;
112
+		$Team = (int)$Team;
113
+		$StartDate = db_string($StartDate);
114
+		$EndDate = db_string($EndDate);
115
+		$QueryID = G::$DB->get_query_id();
116
+		G::$DB->query("
117
+						UPDATE calendar
118
+						SET
119
+							Title = '$Title',
120
+							Body = '$Body',
121
+							Category = '$Category',
122
+							Importance = '$Importance',
123
+							Team = '$Team',
124
+							StartDate = '$StartDate',
125
+							EndDate = '$EndDate'
126
+						WHERE
127
+							ID = '$ID'");
128
+		G::$DB->set_query_id($QueryID);
129
+	}
130
+
131
+	public static function remove_event($ID) {
132
+		$ID = (int)$ID;
133
+		if (!empty($ID)) {
134
+			$QueryID = G::$DB->get_query_id();
135
+			G::$DB->query("DELETE FROM calendar WHERE ID = '$ID'");
136
+			G::$DB->set_query_id($QueryID);
137
+		}
138
+	}
139
+
140
+}

+ 119
- 0
classes/calendarview.class.php View File

1
+<?
2
+
3
+class CalendarView {
4
+	private static $Days = array('S', 'M', 'T', 'W', 'T', 'F', 'S');
5
+	private static $Headings = array('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday');
6
+	private static $Events;
7
+
8
+	public static function render_title($Month, $Year) {
9
+    if (!is_numeric($Month) || !is_numeric($Year)) {
10
+      error(404);
11
+    }
12
+		$NextMonth = $Month % 12 == 0 ? 1 : $Month + 1;
13
+		$PreviousMonth = $Month == 1 ? 12 : $Month - 1;
14
+		$NextYear = $Year;
15
+		if ($NextMonth == 1) {
16
+			$NextYear++;
17
+		}
18
+		$PreviousYear = $Year;
19
+		if ($PreviousMonth == 12) {
20
+			$PreviousYear--;
21
+		}
22
+?>
23
+		<h1 class="center">
24
+			<a href="tools.php?action=calendar&amp;month=<?=$PreviousMonth?>&amp;year=<?=$PreviousYear?>">&lt;</a>
25
+			<?=date("F", mktime(0, 0, 0, $Month, 10)) . " $Year"?>
26
+			<a href="tools.php?action=calendar&amp;month=<?=$NextMonth?>&amp;year=<?=$NextYear?>">&gt;</a>
27
+		</h1>
28
+		<input type="hidden" id="month" value="<?=$Month?>" />
29
+		<input type="hidden" id="year" value="<?=$Year?>" />
30
+<?
31
+	}
32
+
33
+	private static function get_events_on($Day, $Events) {
34
+		// Linear search, Lol.
35
+		$Results = array();
36
+		foreach ($Events as $Event) {
37
+			if ($Event['StartDay'] == $Day || ($Event['StartDay'] <= $Day && $Event['EndDay'] >= $Day)) {
38
+				$Results[] = $Event;
39
+			}
40
+		}
41
+		return $Results;
42
+	}
43
+
44
+
45
+	private static function render_events_day($Day, $Events) {
46
+		$Events = self::get_events_on($Day, $Events);
47
+		foreach ($Events as $Event) {
48
+			$Color = Calendar::$Colors[Calendar::$Importances[$Event['Importance']]];
49
+			$Category = Calendar::$Categories[$Event['Category']];
50
+			$Tooltip = $Event['Title'] . " - " . Calendar::$Categories[$Event['Category']] . " - " . Calendar::$Importances[$Event['Importance']];
51
+?>
52
+			<p><a href="#" class="event_day tooltip" title="<?=$Tooltip?>" data-gazelle-id="<?=$Event['ID']?>" style="color: <?=$Color?>;"><?=Format::cut_string($Event['Title'], 8, true)?></a></p>
53
+<?
54
+		}
55
+	}
56
+
57
+	public static function render_calendar($Month, $Year, $Events) {
58
+		$RunningDay = date('w', mktime(0, 0, 0, $Month, 1, $Year));
59
+		$DaysInMonth = date('t', mktime(0 ,0 ,0, $Month, 1, $Year));
60
+		$DaysThisWeek = 1;
61
+		$DayCounter = 0;
62
+		$DatesArray = array();
63
+?>
64
+
65
+		<table class="calendar">
66
+			<tr>
67
+<?		foreach (self::$Headings as $Heading) { ?>
68
+				<td class="calendar-row calendar-heading">
69
+					<strong><?=$Heading?></strong>
70
+				</td>
71
+<?		} ?>
72
+			</tr>
73
+			<tr class="calendar-row">
74
+
75
+<?		for ($x = 0; $x < $RunningDay; $x++) { ?>
76
+				<td class="calendar-day-np"></td>
77
+<?
78
+			$DaysThisWeek++;
79
+		}
80
+
81
+		for ($i = 1; $i <= $DaysInMonth; $i++) {
82
+?>
83
+				<td class="calendar-day">
84
+					<div class="day-events">
85
+<?						self::render_events_day($i, $Events); ?>
86
+					</div>
87
+					<div class="day-number">
88
+						<?=$i?>
89
+					</div>
90
+				</td>
91
+<?			if ($RunningDay == 6) { ?>
92
+			</tr>
93
+<?				if (($DayCounter + 1) != $DaysInMonth) { ?>
94
+			<tr class="calendar-row">
95
+<?
96
+				}
97
+				$RunningDay = -1;
98
+				$DaysThisWeek = 0;
99
+			}
100
+			$DaysThisWeek++;
101
+			$RunningDay++;
102
+			$DayCounter++;
103
+		}
104
+
105
+		if ($DaysThisWeek < 8) {
106
+			for ($x = 1; $x <= (8 - $DaysThisWeek); $x++) {
107
+?>
108
+				<td class="calendar-day-np"></td>
109
+<?
110
+			}
111
+		}
112
+?>
113
+			</tr>
114
+
115
+		</table>
116
+<?
117
+		echo $Calendar;
118
+	}
119
+}

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

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

+ 35
- 0
classes/classloader.php View File

1
+<?
2
+/**
3
+ * Load classes automatically when they're needed
4
+ *
5
+ * @param string $ClassName class name
6
+ */
7
+spl_autoload_register(function ($ClassName) {
8
+	$FilePath = SERVER_ROOT . '/classes/' . strtolower($ClassName) . '.class.php';
9
+	if (!file_exists($FilePath)) {
10
+		// TODO: Rename the following classes to conform with the code guidelines
11
+		switch ($ClassName) {
12
+			case 'MASS_USER_BOOKMARKS_EDITOR':
13
+				$FileName = 'mass_user_bookmarks_editor.class';
14
+				break;
15
+			case 'MASS_USER_TORRENTS_EDITOR':
16
+				$FileName = 'mass_user_torrents_editor.class';
17
+				break;
18
+			case 'MASS_USER_TORRENTS_TABLE_VIEW':
19
+				$FileName = 'mass_user_torrents_table_view.class';
20
+			    break;
21
+			case 'TEXTAREA_PREVIEW':
22
+				$FileName = 'textarea_preview.class';
23
+				break;
24
+			case 'TORRENT':
25
+			case 'BENCODE_DICT':
26
+			case 'BENCODE_LIST':
27
+				$FileName = 'torrent.class';
28
+				break;
29
+			default:
30
+				die("Couldn't import class $ClassName");
31
+		}
32
+		$FilePath = SERVER_ROOT . "/classes/$FileName.php";
33
+	}
34
+	require_once($FilePath);
35
+});

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

1
+<?
2
+class Collages {
3
+	public static function increase_subscriptions($CollageID) {
4
+		$QueryID = G::$DB->get_query_id();
5
+		G::$DB->query("
6
+			UPDATE collages
7
+			SET Subscribers = Subscribers + 1
8
+			WHERE ID = '$CollageID'");
9
+		G::$DB->set_query_id($QueryID);
10
+	}
11
+
12
+	public static function decrease_subscriptions($CollageID) {
13
+		$QueryID = G::$DB->get_query_id();
14
+		G::$DB->query("
15
+			UPDATE collages
16
+			SET Subscribers = IF(Subscribers < 1, 0, Subscribers - 1)
17
+			WHERE ID = '$CollageID'");
18
+		G::$DB->set_query_id($QueryID);
19
+	}
20
+
21
+	public static function create_personal_collage() {
22
+		G::$DB->query("
23
+			SELECT
24
+				COUNT(ID)
25
+			FROM collages
26
+			WHERE UserID = '" . G::$LoggedUser['ID'] . "'
27
+				AND CategoryID = '0'
28
+				AND Deleted = '0'");
29
+		list($CollageCount) = G::$DB->next_record();
30
+
31
+		if ($CollageCount >= G::$LoggedUser['Permissions']['MaxCollages']) {
32
+			// TODO: fix this, the query was for COUNT(ID), so I highly doubt that this works... - Y
33
+			list($CollageID) = G::$DB->next_record();
34
+			header('Location: collage.php?id='.$CollageID);
35
+			die();
36
+		}
37
+		$NameStr = db_string(G::$LoggedUser['Username'] . "'s personal collage" . ($CollageCount > 0 ? ' no. ' . ($CollageCount + 1) : ''));
38
+		$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].');
39
+		G::$DB->query("
40
+			INSERT INTO collages
41
+				(Name, Description, CategoryID, UserID)
42
+			VALUES
43
+				('$NameStr', '$Description', '0', " . G::$LoggedUser['ID'] . ")");
44
+		$CollageID = G::$DB->inserted_id();
45
+		header('Location: collage.php?id='.$CollageID);
46
+		die();
47
+	}
48
+}

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

1
+<?
2
+class Comments {
3
+	/*
4
+	 * For all functions:
5
+	 * $Page = 'artist', 'collages', 'requests' or 'torrents'
6
+	 * $PageID = ArtistID, CollageID, RequestID or GroupID, respectively
7
+	 */
8
+
9
+	/**
10
+	 * Post a comment on an artist, request or torrent page.
11
+	 * @param string $Page
12
+	 * @param int $PageID
13
+	 * @param string $Body
14
+	 * @return int ID of the new comment
15
+	 */
16
+	public static function post($Page, $PageID, $Body) {
17
+		$QueryID = G::$DB->get_query_id();
18
+		G::$DB->query("
19
+			SELECT
20
+				CEIL(
21
+					(
22
+						SELECT COUNT(ID) + 1
23
+						FROM comments
24
+						WHERE Page = '$Page'
25
+							AND PageID = $PageID
26
+					) / " . TORRENT_COMMENTS_PER_PAGE . "
27
+				) AS Pages");
28
+		list($Pages) = G::$DB->next_record();
29
+
30
+		G::$DB->query("
31
+			INSERT INTO comments (Page, PageID, AuthorID, AddedTime, Body)
32
+			VALUES ('$Page', $PageID, " . G::$LoggedUser['ID'] . ", '" . sqltime() . "', '" . db_string($Body) . "')");
33
+		$PostID = G::$DB->inserted_id();
34
+
35
+		$CatalogueID = floor((TORRENT_COMMENTS_PER_PAGE * $Pages - TORRENT_COMMENTS_PER_PAGE) / THREAD_CATALOGUE);
36
+		G::$Cache->delete_value($Page.'_comments_'.$PageID.'_catalogue_'.$CatalogueID);
37
+		G::$Cache->delete_value($Page.'_comments_'.$PageID);
38
+
39
+		Subscriptions::flush_subscriptions($Page, $PageID);
40
+		Subscriptions::quote_notify($Body, $PostID, $Page, $PageID);
41
+
42
+		G::$DB->set_query_id($QueryID);
43
+
44
+		return $PostID;
45
+	}
46
+
47
+	/**
48
+	 * Edit a comment
49
+	 * @param int $PostID
50
+	 * @param string $NewBody
51
+	 * @param bool $SendPM If true, send a PM to the author of the comment informing him about the edit
52
+	 * @todo move permission check out of here/remove hardcoded error(404)
53
+	 */
54
+	public static function edit($PostID, $NewBody, $SendPM = false) {
55
+		$QueryID = G::$DB->get_query_id();
56
+
57
+		G::$DB->query("
58
+			SELECT
59
+				Body,
60
+				AuthorID,
61
+				Page,
62
+				PageID,
63
+				AddedTime
64
+			FROM comments
65
+			WHERE ID = $PostID");
66
+		if (!G::$DB->has_results()) {
67
+			return false;
68
+		}
69
+		list($OldBody, $AuthorID, $Page, $PageID, $AddedTime) = G::$DB->next_record();
70
+
71
+		if (G::$LoggedUser['ID'] != $AuthorID && !check_perms('site_moderate_forums')) {
72
+			return false;
73
+		}
74
+
75
+		G::$DB->query("
76
+			SELECT CEIL(COUNT(ID) / " . TORRENT_COMMENTS_PER_PAGE . ") AS Page
77
+			FROM comments
78
+			WHERE Page = '$Page'
79
+				AND PageID = $PageID
80
+				AND ID <= $PostID");
81
+		list($CommPage) = G::$DB->next_record();
82
+
83
+		// Perform the update
84
+		G::$DB->query("
85
+			UPDATE comments
86
+			SET
87
+				Body = '" . db_string($NewBody) . "',
88
+				EditedUserID = " . G::$LoggedUser['ID'] . ",
89
+				EditedTime = '" . sqltime() . "'
90
+			WHERE ID = $PostID");
91
+
92
+		// Update the cache
93
+		$CatalogueID = floor((TORRENT_COMMENTS_PER_PAGE * $CommPage - TORRENT_COMMENTS_PER_PAGE) / THREAD_CATALOGUE);
94
+		G::$Cache->delete_value($Page . '_comments_' . $PageID . '_catalogue_' . $CatalogueID);
95
+
96
+		if ($Page == 'collages') {
97
+			// On collages, we also need to clear the collage key (collage_$CollageID), because it has the comments in it... (why??)
98
+			G::$Cache->delete_value('collage_' . $PageID);
99
+		}
100
+
101
+		G::$DB->query("
102
+			INSERT INTO comments_edits (Page, PostID, EditUser, EditTime, Body)
103
+			VALUES ('$Page', $PostID, " . G::$LoggedUser['ID'] . ", '" . sqltime() . "', '" . db_string($OldBody) . "')");
104
+
105
+		G::$DB->set_query_id($QueryID);
106
+
107
+		if ($SendPM && G::$LoggedUser['ID'] != $AuthorID) {
108
+			// Send a PM to the user to notify them of the edit
109
+			$PMSubject = "Your comment #$PostID has been edited";
110
+			$PMurl = site_url()."comments.php?action=jump&postid=$PostID";
111
+			$ProfLink = '[url='.site_url().'user.php?id='.G::$LoggedUser['ID'].']'.G::$LoggedUser['Username'].'[/url]';
112
+			$PMBody = "One of your comments has been edited by $ProfLink: [url]{$PMurl}[/url]";
113
+			Misc::send_pm($AuthorID, 0, $PMSubject, $PMBody);
114
+		}
115
+
116
+		return true; // TODO: this should reflect whether or not the update was actually successful, e.g. by checking G::$DB->affected_rows after the UPDATE query
117
+	}
118
+
119
+	/**
120
+	 * Delete a comment
121
+	 * @param int $PostID
122
+	 */
123
+	public static function delete($PostID) {
124
+		$QueryID = G::$DB->get_query_id();
125
+		// Get page, pageid
126
+		G::$DB->query("SELECT Page, PageID FROM comments WHERE ID = $PostID");
127
+		if (!G::$DB->has_results()) {
128
+			// no such comment?
129
+			G::$DB->set_query_id($QueryID);
130
+			return false;
131
+		}
132
+		list ($Page, $PageID) = G::$DB->next_record();
133
+		// get number of pages
134
+		G::$DB->query("
135
+			SELECT
136
+				CEIL(COUNT(ID) / " . TORRENT_COMMENTS_PER_PAGE . ") AS Pages,
137
+				CEIL(SUM(IF(ID <= $PostID, 1, 0)) / " . TORRENT_COMMENTS_PER_PAGE . ") AS Page
138
+			FROM comments
139
+			WHERE Page = '$Page'
140
+				AND PageID = $PageID
141
+			GROUP BY PageID");
142
+		if (!G::$DB->has_results()) {
143
+			// the comment $PostID was probably not posted on $Page
144
+			G::$DB->set_query_id($QueryID);
145
+			return false;
146
+		}
147
+		list($CommPages, $CommPage) = G::$DB->next_record();
148
+
149
+		// $CommPages = number of pages in the thread
150
+		// $CommPage = which page the post is on
151
+		// These are set for cache clearing.
152
+
153
+		G::$DB->query("
154
+			DELETE FROM comments
155
+			WHERE ID = $PostID");
156
+		G::$DB->query("
157
+			DELETE FROM comments_edits
158
+			WHERE Page = '$Page'
159
+				AND PostID = $PostID");
160
+
161
+		G::$DB->query("
162
+			DELETE FROM users_notify_quoted
163
+			WHERE Page = '$Page'
164
+				AND PostID = $PostID");
165
+
166
+		Subscriptions::flush_subscriptions($Page, $PageID);
167
+		Subscriptions::flush_quote_notifications($Page, $PageID);
168
+
169
+		//We need to clear all subsequential catalogues as they've all been bumped with the absence of this post
170
+		$ThisCatalogue = floor((TORRENT_COMMENTS_PER_PAGE * $CommPage - TORRENT_COMMENTS_PER_PAGE) / THREAD_CATALOGUE);
171
+		$LastCatalogue = floor((TORRENT_COMMENTS_PER_PAGE * $CommPages - TORRENT_COMMENTS_PER_PAGE) / THREAD_CATALOGUE);
172
+		for ($i = $ThisCatalogue; $i <= $LastCatalogue; ++$i) {
173
+			G::$Cache->delete_value($Page . '_comments_' . $PageID . '_catalogue_' . $i);
174
+		}
175
+
176
+		G::$Cache->delete_value($Page . '_comments_' . $PageID);
177
+
178
+		if ($Page == 'collages') {
179
+			// On collages, we also need to clear the collage key (collage_$CollageID), because it has the comments in it... (why??)
180
+			G::$Cache->delete_value("collage_$PageID");
181
+		}
182
+
183
+		G::$DB->set_query_id($QueryID);
184
+
185
+		return true;
186
+	}
187
+
188
+	/**
189
+	 * Get the URL to a comment, already knowing the Page and PostID
190
+	 * @param string $Page
191
+	 * @param int $PageID
192
+	 * @param int $PostID
193
+	 * @return string|bool The URL to the comment or false on error
194
+	 */
195
+	public static function get_url($Page, $PageID, $PostID = null) {
196
+		$Post = (!empty($PostID) ? "&postid=$PostID#post$PostID" : '');
197
+		switch ($Page) {
198
+			case 'artist':
199
+				return "artist.php?id=$PageID$Post";
200
+			case 'collages':
201
+				return "collages.php?action=comments&collageid=$PageID$Post";
202
+			case 'requests':
203
+				return "requests.php?action=view&id=$PageID$Post";
204
+			case 'torrents':
205
+				return "torrents.php?id=$PageID$Post";
206
+			default:
207
+				return false;
208
+		}
209
+	}
210
+
211
+	/**
212
+	 * Get the URL to a comment
213
+	 * @param int $PostID
214
+	 * @return string|bool The URL to the comment or false on error
215
+	 */
216
+	public static function get_url_query($PostID) {
217
+		$QueryID = G::$DB->get_query_id();
218
+
219
+		G::$DB->query("
220
+			SELECT Page, PageID
221
+			FROM comments
222
+			WHERE ID = $PostID");
223
+		if (!G::$DB->has_results()) {
224
+			error(404);
225
+		}
226
+		list($Page, $PageID) = G::$DB->next_record();
227
+
228
+		G::$DB->set_query_id($QueryID);
229
+
230
+		return self::get_url($Page, $PageID, $PostID);
231
+	}
232
+
233
+	/**
234
+	 * Load a page's comments. This takes care of `postid` and (indirectly) `page` parameters passed in $_GET.
235
+	 * Quote notifications and last read are also handled here, unless $HandleSubscriptions = false is passed.
236
+	 * @param string $Page
237
+	 * @param int $PageID
238
+	 * @param bool $HandleSubscriptions Whether or not to handle subscriptions (last read & quote notifications)
239
+	 * @return array ($NumComments, $Page, $Thread, $LastRead)
240
+	 *     $NumComments: the total number of comments on this artist/request/torrent group
241
+	 *     $Page: the page we're currently on
242
+	 *     $Thread: an array of all posts on this page
243
+	 *     $LastRead: ID of the last comment read by the current user in this thread;
244
+	 *                will be false if $HandleSubscriptions == false or if there are no comments on this page
245
+	 */
246
+	public static function load($Page, $PageID, $HandleSubscriptions = true) {
247
+		$QueryID = G::$DB->get_query_id();
248
+
249
+		// Get the total number of comments
250
+		$NumComments = G::$Cache->get_value($Page."_comments_$PageID");
251
+		if ($NumComments === false) {
252
+			G::$DB->query("
253
+				SELECT COUNT(ID)
254
+				FROM comments
255
+				WHERE Page = '$Page'
256
+					AND PageID = $PageID");
257
+			list($NumComments) = G::$DB->next_record();
258
+			G::$Cache->cache_value($Page."_comments_$PageID", $NumComments, 0);
259
+		}
260
+
261
+		// If a postid was passed, we need to determine which page that comment is on.
262
+		// Format::page_limit handles a potential $_GET['page']
263
+		if (isset($_GET['postid']) && is_number($_GET['postid']) && $NumComments > TORRENT_COMMENTS_PER_PAGE) {
264
+			G::$DB->query("
265
+				SELECT COUNT(ID)
266
+				FROM comments
267
+				WHERE Page = '$Page'
268
+					AND PageID = $PageID
269
+					AND ID <= $_GET[postid]");
270
+			list($PostNum) = G::$DB->next_record();
271
+			list($CommPage, $Limit) = Format::page_limit(TORRENT_COMMENTS_PER_PAGE, $PostNum);
272
+		} else {
273
+			list($CommPage, $Limit) = Format::page_limit(TORRENT_COMMENTS_PER_PAGE, $NumComments);
274
+		}
275
+
276
+		// Get the cache catalogue
277
+		$CatalogueID = floor((TORRENT_COMMENTS_PER_PAGE * $CommPage - TORRENT_COMMENTS_PER_PAGE) / THREAD_CATALOGUE);
278
+
279
+		// Cache catalogue from which the page is selected, allows block caches and future ability to specify posts per page
280
+		$Catalogue = G::$Cache->get_value($Page.'_comments_'.$PageID.'_catalogue_'.$CatalogueID);
281
+		if ($Catalogue === false) {
282
+			$CatalogueLimit = $CatalogueID * THREAD_CATALOGUE . ', ' . THREAD_CATALOGUE;
283
+			G::$DB->query("
284
+				SELECT
285
+					c.ID,
286
+					c.AuthorID,
287
+					c.AddedTime,
288
+					c.Body,
289
+					c.EditedUserID,
290
+					c.EditedTime,
291
+					u.Username
292
+				FROM comments AS c
293
+					LEFT JOIN users_main AS u ON u.ID = c.EditedUserID
294
+				WHERE c.Page = '$Page'
295
+					AND c.PageID = $PageID
296
+				ORDER BY c.ID
297
+				LIMIT $CatalogueLimit");
298
+			$Catalogue = G::$DB->to_array(false, MYSQLI_ASSOC);
299
+			G::$Cache->cache_value($Page.'_comments_'.$PageID.'_catalogue_'.$CatalogueID, $Catalogue, 0);
300
+		}
301
+
302
+		//This is a hybrid to reduce the catalogue down to the page elements: We use the page limit % catalogue
303
+		$Thread = array_slice($Catalogue, ((TORRENT_COMMENTS_PER_PAGE * $CommPage - TORRENT_COMMENTS_PER_PAGE) % THREAD_CATALOGUE), TORRENT_COMMENTS_PER_PAGE, true);
304
+
305
+		if ($HandleSubscriptions && count($Thread) > 0) {
306
+			// quote notifications
307
+			$LastPost = end($Thread);
308
+			$LastPost = $LastPost['ID'];
309
+			$FirstPost = reset($Thread);
310
+			$FirstPost = $FirstPost['ID'];
311
+			G::$DB->query("
312
+				UPDATE users_notify_quoted
313
+				SET UnRead = false
314
+				WHERE UserID = " . G::$LoggedUser['ID'] . "
315
+					AND Page = '$Page'
316
+					AND PageID = $PageID
317
+					AND PostID >= $FirstPost
318
+					AND PostID <= $LastPost");
319
+			if (G::$DB->affected_rows()) {
320
+				G::$Cache->delete_value('notify_quoted_' . G::$LoggedUser['ID']);
321
+			}
322
+
323
+			// last read
324
+			G::$DB->query("
325
+				SELECT PostID
326
+				FROM users_comments_last_read
327
+				WHERE UserID = " . G::$LoggedUser['ID'] . "
328
+					AND Page = '$Page'
329
+					AND PageID = $PageID");
330
+			list($LastRead) = G::$DB->next_record();
331
+			if ($LastRead < $LastPost) {
332
+				G::$DB->query("
333
+					INSERT INTO users_comments_last_read
334
+						(UserID, Page, PageID, PostID)
335
+					VALUES
336
+						(" . G::$LoggedUser['ID'] . ", '$Page', $PageID, $LastPost)
337
+					ON DUPLICATE KEY UPDATE
338
+						PostID = $LastPost");
339
+				G::$Cache->delete_value('subscriptions_user_new_' . G::$LoggedUser['ID']);
340
+			}
341
+		} else {
342
+			$LastRead = false;
343
+		}
344
+
345
+		G::$DB->set_query_id($QueryID);
346
+
347
+		return array($NumComments, $CommPage, $Thread, $LastRead);
348
+	}
349
+
350
+	/**
351
+	 * Merges all comments from $Page/$PageID into $Page/$TargetPageID. This also takes care of quote notifications, subscriptions and cache.
352
+	 * @param type $Page
353
+	 * @param type $PageID
354
+	 * @param type $TargetPageID
355
+	 */
356
+	public static function merge($Page, $PageID, $TargetPageID) {
357
+		$QueryID = G::$DB->get_query_id();
358
+
359
+		G::$DB->query("
360
+			UPDATE comments
361
+			SET PageID = $TargetPageID
362
+			WHERE Page = '$Page'
363
+				AND PageID = $PageID");
364
+
365
+		// quote notifications
366
+		G::$DB->query("
367
+			UPDATE users_notify_quoted
368
+			SET PageID = $TargetPageID
369
+			WHERE Page = '$Page'
370
+				AND PageID = $PageID");
371
+
372
+		// comment subscriptions
373
+		Subscriptions::move_subscriptions($Page, $PageID, $TargetPageID);
374
+
375
+		// cache (we need to clear all comment catalogues)
376
+		G::$DB->query("
377
+			SELECT
378
+				CEIL(COUNT(ID) / " . TORRENT_COMMENTS_PER_PAGE . ") AS Pages
379
+			FROM comments
380
+			WHERE Page = '$Page'
381
+				AND PageID = $TargetPageID
382
+			GROUP BY PageID");
383
+		list($CommPages) = G::$DB->next_record();
384
+		$LastCatalogue = floor((TORRENT_COMMENTS_PER_PAGE * $CommPages - TORRENT_COMMENTS_PER_PAGE) / THREAD_CATALOGUE);
385
+		for ($i = 0; $i <= $LastCatalogue; ++$i) {
386
+			G::$Cache->delete_value($Page . "_comments_$TargetPageID" . "_catalogue_$i");
387
+		}
388
+		G::$Cache->delete_value($Page."_comments_$TargetPageID");
389
+		G::$DB->set_query_id($QueryID);
390
+	}
391
+
392
+	/**
393
+	 * Delete all comments on $Page/$PageID (deals with quote notifications and subscriptions as well)
394
+	 * @param string $Page
395
+	 * @param int $PageID
396
+	 * @return boolean
397
+	 */
398
+	public static function delete_page($Page, $PageID) {
399
+		$QueryID = G::$DB->get_query_id();
400
+
401
+		// get number of pages
402
+		G::$DB->query("
403
+			SELECT
404
+				CEIL(COUNT(ID) / " . TORRENT_COMMENTS_PER_PAGE . ") AS Pages
405
+			FROM comments
406
+			WHERE Page = '$Page'
407
+				AND PageID = $PageID
408
+			GROUP BY PageID");
409
+		if (!G::$DB->has_results()) {
410
+			return false;
411
+		}
412
+		list($CommPages) = G::$DB->next_record();
413
+
414
+		// Delete comments
415
+		G::$DB->query("
416
+			DELETE FROM comments
417
+			WHERE Page = '$Page'
418
+				AND PageID = $PageID");
419
+
420
+		// Delete quote notifications
421
+		Subscriptions::flush_quote_notifications($Page, $PageID);
422
+		G::$DB->query("
423
+			DELETE FROM users_notify_quoted
424
+			WHERE Page = '$Page'
425
+				AND PageID = $PageID");
426
+
427
+		// Deal with subscriptions
428
+		Subscriptions::move_subscriptions($Page, $PageID, null);
429
+
430
+		// Clear cache
431
+		$LastCatalogue = floor((TORRENT_COMMENTS_PER_PAGE * $CommPages - TORRENT_COMMENTS_PER_PAGE) / THREAD_CATALOGUE);
432
+		for ($i = 0; $i <= $LastCatalogue; ++$i) {
433
+			G::$Cache->delete_value($Page . '_comments_' . $PageID . '_catalogue_' . $i);
434
+		}
435
+		G::$Cache->delete_value($Page.'_comments_'.$PageID);
436
+
437
+		G::$DB->set_query_id($QueryID);
438
+
439
+		return true;
440
+	}
441
+}

+ 95
- 0
classes/commentsview.class.php View File

1
+<?
2
+class CommentsView {
3
+	/**
4
+	 * Render a thread of comments
5
+	 * @param array $Thread An array as returned by Comments::load
6
+	 * @param int $LastRead PostID of the last read post
7
+	 * @param string $Baselink Link to the site these comments are on
8
+	 */
9
+	public static function render_comments($Thread, $LastRead, $Baselink) {
10
+		foreach ($Thread as $Post) {
11
+			list($PostID, $AuthorID, $AddedTime, $CommentBody, $EditedUserID, $EditedTime, $EditedUsername) = array_values($Post);
12
+			self::render_comment($AuthorID, $PostID, $CommentBody, $AddedTime, $EditedUserID, $EditedTime, $Baselink . "&amp;postid=$PostID#post$PostID", ($PostID > $LastRead));
13
+		}
14
+	}
15
+
16
+	/**
17
+	 * Render one comment
18
+	 * @param int $AuthorID
19
+	 * @param int $PostID
20
+	 * @param string $Body
21
+	 * @param string $AddedTime
22
+	 * @param int $EditedUserID
23
+	 * @param string $EditedTime
24
+	 * @param string $Link The link to the post elsewhere on the site
25
+	 * @param string $Header The header used in the post
26
+	 * @param bool $Tools Whether or not to show [Edit], [Report] etc.
27
+	 * @todo Find a better way to pass the page (artist, collages, requests, torrents) to this function than extracting it from $Link
28
+	 */
29
+	static function render_comment($AuthorID, $PostID, $Body, $AddedTime, $EditedUserID, $EditedTime, $Link, $Unread = false, $Header = '', $Tools = true) {
30
+		$UserInfo = Users::user_info($AuthorID);
31
+		$Header = '<strong>' . Users::format_username($AuthorID, true, true, true, true, false) . '</strong> ' . time_diff($AddedTime) . $Header;
32
+?>
33
+		<table class="forum_post box vertical_margin<?=(!Users::has_avatars_enabled() ? ' noavatar' : '') . ($Unread ? ' forum_unread' : '')?>" id="post<?=$PostID?>">
34
+			<colgroup>
35
+<?		if (Users::has_avatars_enabled()) { ?>
36
+				<col class="col_avatar" />
37
+<?		} ?>
38
+				<col class="col_post_body" />
39
+			</colgroup>
40
+			<tr class="colhead_dark">
41
+				<td colspan="<?=(Users::has_avatars_enabled() ? 2 : 1)?>">
42
+					<div style="float: left;"><a class="post_id" href="<?=$Link?>">#<?=$PostID?></a>
43
+						<?=$Header?>
44
+<?		if ($Tools) { ?>
45
+						- <a href="#quickpost" onclick="Quote('<?=$PostID?>','<?=$UserInfo['Username']?>', true);" class="brackets">Quote</a>
46
+<?			if ($AuthorID == G::$LoggedUser['ID'] || check_perms('site_moderate_forums')) { ?>
47
+						- <a href="#post<?=$PostID?>" onclick="Edit_Form('<?=$PostID?>','');" class="brackets">Edit</a>
48
+<?			}
49
+			if (check_perms('site_moderate_forums')) { ?>
50
+						- <a href="#post<?=$PostID?>" onclick="Delete('<?=$PostID?>');" class="brackets">Delete</a>
51
+<?			} ?>
52
+					</div>
53
+					<div id="bar<?=$PostID?>" style="float: right;">
54
+						<a href="reports.php?action=report&amp;type=comment&amp;id=<?=$PostID?>" class="brackets">Report</a>
55
+<?
56
+			if (check_perms('users_warn') && $AuthorID != G::$LoggedUser['ID'] && G::$LoggedUser['Class'] >= $UserInfo['Class']) {
57
+?>
58
+						<form class="manage_form hidden" name="user" id="warn<?=$PostID?>" action="comments.php" method="post">
59
+							<input type="hidden" name="action" value="warn" />
60
+							<input type="hidden" name="postid" value="<?=$PostID?>" />
61
+						</form>
62
+						- <a href="#" onclick="$('#warn<?=$PostID?>').raw().submit(); return false;" class="brackets">Warn</a>
63
+<?			} ?>
64
+						&nbsp;
65
+						<a href="#">&uarr;</a>
66
+<?		} ?>
67
+					</div>
68
+				</td>
69
+			</tr>
70
+			<tr>
71
+<?		if (Users::has_avatars_enabled()) { ?>
72
+				<td class="avatar" valign="top">
73
+				<?=Users::show_avatar($UserInfo['Avatar'], $AuthorID, $UserInfo['Username'], G::$LoggedUser['DisableAvatars'])?>
74
+				</td>
75
+<?		} ?>
76
+				<td class="body" valign="top">
77
+					<div id="content<?=$PostID?>">
78
+						<?=Text::full_format($Body)?>
79
+<?		if ($EditedUserID) { ?>
80
+						<br />
81
+						<br />
82
+            <div class="last_edited">
83
+<?			if (check_perms('site_admin_forums')) { ?>
84
+              <a href="#content<?=$PostID?>" onclick="LoadEdit('<?=substr($Link, 0, strcspn($Link, '.'))?>', <?=$PostID?>, 1); return false;">&laquo;</a>
85
+<?			} ?>
86
+              Last edited by
87
+              <?=Users::format_username($EditedUserID, false, false, false) ?> <?=time_diff($EditedTime, 2, true, true)?>
88
+<?		} ?>
89
+            </div>
90
+					</div>
91
+				</td>
92
+			</tr>
93
+		</table>
94
+<?	}
95
+}

+ 276
- 0
classes/config.template View File

1
+<?php
2
+if (PHP_VERSION_ID < 70000) {
3
+	die("Gazelle requires PHP 7.0 or later to function properly");
4
+}
5
+//date_default_timezone_set('EST');
6
+
7
+// Main settings
8
+define('SITE_NAME', 'Oppaitime'); //The name of your site
9
+define('NONSSL_SITE_URL', 'oppaiti.me'); //The FQDN of your site
10
+define('SSL_SITE_URL', 'oppaiti.me'); //The FQDN of your site, make this different if you are using a subdomain for ssl
11
+define('SITE_IP', '8.8.8.8'); //The IP address by which your site can be publicly accessed
12
+define('SERVER_ROOT', '/var/www'); //The root of the server, used for includes, purpose is to shorten the path string
13
+
14
+// Tracker urls to be added to torrent files ala bittorrent.org/beps/bep_0012.html
15
+define('ANNOUNCE_URLS', [['https://tracker.'.SSL_SITE_URL.':34001',
16
+                   'https://tracker2.'.SSL_SITE_URL.':34001',
17
+                   'https://tracker3.'.SSL_SITE_URL.':34001',
18
+                   'https://tracker4.'.SSL_SITE_URL.':34001']] );
19
+
20
+// Don't hard code API keys for services
21
+define('API_KEYS', ['ANIDB' => 'AAAAAAAAAAAAAAAA']);
22
+
23
+// Allows you to run static content off another server. Default is usually what you want.
24
+define('NONSSL_STATIC_SERVER', 'static/');
25
+define('SSL_STATIC_SERVER', 'static/');
26
+
27
+// Keys
28
+define('ENCKEY', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); //Random key. The key for encryption
29
+define('SCHEDULE_KEY', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); // Random key. This key must be the argument to schedule.php for the schedule to work.
30
+define('RSS_HASH', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); //Random key. Used for generating unique RSS auth key.
31
+
32
+// MySQL details
33
+define('SQLHOST', 'localhost'); //The MySQL host ip/fqdn
34
+define('SQLLOGIN', 'root');//The MySQL login
35
+define('SQLPASS', 'toor'); //The MySQL password
36
+define('SQLDB', 'gazelle'); //The MySQL database to use
37
+define('SQLPORT', 3306); //The MySQL port to connect on
38
+define('SQLSOCK', '/var/run/mysqld/mysqld.sock');
39
+
40
+// Memcached details
41
+define('MEMCACHED_SERVERS',
42
+	[['host' => 'unix:///var/run/memcached.sock', 'port' => 0, 'buckets' => 1]]
43
+);
44
+
45
+// Sphinx details
46
+define('SPHINX_HOST', 'localhost');
47
+define('SPHINX_PORT', 9312);
48
+define('SPHINXQL_HOST', '127.0.0.1');
49
+define('SPHINXQL_PORT', 9306);
50
+define('SPHINXQL_SOCK', false);
51
+define('SPHINX_MAX_MATCHES', 1000); // Must be <= the server's max_matches variable (default 1000)
52
+define('SPHINX_INDEX', 'torrents');
53
+
54
+// Ocelot details
55
+define('TRACKER_HOST', '8.8.8.8');
56
+define('TRACKER_PORT', 34000);
57
+define('TRACKER_SECRET', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); // Must be 32 characters and match site_password in Ocelot's config.cpp
58
+define('TRACKER_REPORTKEY', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); // Must be 32 characters and match report_password in Ocelot's config.cpp
59
+
60
+if (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 80) {
61
+	define('SITE_URL', NONSSL_SITE_URL);
62
+	define('STATIC_SERVER', NONSSL_STATIC_SERVER);
63
+} else {
64
+	define('SITE_URL', SSL_SITE_URL);
65
+	define('STATIC_SERVER', SSL_STATIC_SERVER);
66
+}
67
+
68
+// Site settings
69
+define('DEBUG_MODE', false); //Set to false if you dont want everyone to see debug information, can be overriden with 'site_debug'
70
+define('DEBUG_WARNINGS', true); //Set to true if you want to see PHP warnings in the footer
71
+define('OPEN_REGISTRATION', false); //Set to false to disable open regirstration, true to allow anyone to register
72
+define('USER_LIMIT', 0); //The maximum number of users the site can have, 0 for no limit
73
+define('STARTING_INVITES', 0); //# of invites to give to newly registered users
74
+define('BLOCK_TOR', false); //Set to true to block Tor users
75
+define('BLOCK_OPERA_MINI', false); //Set to true to block Opera Mini proxy
76
+define('DONOR_INVITES', 2);
77
+if (!defined('FEATURE_EMAIL_REENABLE')) {
78
+  define('FEATURE_EMAIL_REENABLE', true);
79
+}
80
+
81
+// User class IDs needed for automatic promotions. Found in the 'permissions' table
82
+// Name of class	Class ID (NOT level)
83
+define('ADMIN',		'1');
84
+define('USER',		'2');
85
+define('MEMBER',	'3');
86
+define('POWER',		'4');
87
+define('ELITE',		'5');
88
+define('VIP',		'21');
89
+define('TORRENT_MASTER','23');
90
+define('LEGEND',	'8');
91
+define('MOD',		'11');
92
+define('SYSOP',		'15');
93
+define('ARTIST',	'19');
94
+define('DONOR',		'20');
95
+define('POWER_TM',	'24');
96
+define('FLS_TEAM', '9000');
97
+define('FORUM_MOD', '9001');
98
+
99
+// Forums
100
+define('STAFF_FORUM', 7);
101
+define('DONOR_FORUM', 9);
102
+define('TRASH_FORUM_ID', 4);
103
+define('ANNOUNCEMENT_FORUM_ID', 10);
104
+
105
+// Pagination
106
+define('TORRENT_COMMENTS_PER_PAGE', 10);
107
+define('POSTS_PER_PAGE', 25);
108
+define('TOPICS_PER_PAGE', 50);
109
+define('TORRENTS_PER_PAGE', 50);
110
+define('REQUESTS_PER_PAGE', 25);
111
+define('MESSAGES_PER_PAGE', 25);
112
+define('LOG_ENTRIES_PER_PAGE', 50);
113
+
114
+// Cache catalogues
115
+define('THREAD_CATALOGUE', 500); // Limit to THREAD_CATALOGUE posts per cache key.
116
+
117
+// IRC settings
118
+define('BOT_NICK', 'bot');
119
+define('BOT_SERVER', 'irc.'.SSL_SITE_URL); // IRC server address. Used for onsite chat tool.
120
+define('BOT_PORT', 6667);
121
+define('BOT_CHAN', '#bot');
122
+define('BOT_ANNOUNCE_CHAN', '#announce');
123
+define('BOT_REQUEST_CHAN', '#requests');
124
+define('BOT_STAFF_CHAN', '#staff');
125
+define('BOT_DISABLED_CHAN', '#disabled'); // Channel to refer disabled users to.
126
+define('BOT_HELP_CHAN', '#help');
127
+define('BOT_DEBUG_CHAN', '#debup');
128
+define('BOT_REPORT_CHAN', '#report');
129
+define('BOT_NICKSERV_PASS', '');
130
+define('BOT_INVITE_CHAN', '#invites'); // Channel for non-members seeking an interview
131
+define('BOT_INTERVIEW_CHAN', '#interview'); // Channel for the interviews
132
+define('BOT_INTERVIEW_NUM', 5);
133
+define('BOT_INTERVIEW_STAFF', '#interview-staff'); // Channel for the interviewers
134
+define('SOCKET_LISTEN_PORT', 51010);
135
+define('SOCKET_LISTEN_ADDRESS', '8.8.8.8');
136
+define('ADMIN_CHAN', '#admin');
137
+define('LAB_CHAN', '#lab');
138
+define('STATUS_CHAN', '#status');
139
+
140
+// Miscellaneous values
141
+define('RANK_ONE_COST', 5);
142
+define('RANK_TWO_COST', 10);
143
+define('RANK_THREE_COST', 15);
144
+define('RANK_FOUR_COST', 20);
145
+define('RANK_FIVE_COST', 30);
146
+define('MAX_RANK', 6);
147
+define('MAX_EXTRA_RANK', 8);
148
+define('DONOR_FORUM_RANK', 6);
149
+define('MAX_SPECIAL_RANK', 3);
150
+
151
+$ForumsRevealVoters = array();
152
+$ForumsDoublePost = array();
153
+
154
+$Categories = array('Movies', 'Anime', 'Manga', 'Games', 'Other');
155
+$GroupedCategories = $Categories;
156
+$CategoryIcons = array('music.png', 'apps.png', 'ebook.png', 'audiobook.png', 'elearning.png', 'comedy.png', 'comics.png');
157
+
158
+$Media = array('TV', 'DVD', 'Bluray', 'HD DVD', 'VHS', 'VCD', 'LD', 'Web');
159
+$MediaManga = array('Scan', 'Web');
160
+$Platform = array('Windows', 'OS X', 'Linux', 'BSD', 'Flash', 'Java', 'Android', 'iOS');
161
+$Containers = array('AVI', 'MKV', 'MP4', 'OGM', 'WMV', 'ISO', 'VOB IFO', 'TS', 'M2TS');
162
+$ContainersGames = array('ISO', 'BIN-CUE', 'Installer', 'Loose');
163
+$Codecs = array('h264', '10-bit h264', 'XviD', 'DivX', 'WMV', 'DVD5', 'DVD9', 'HEVC', 'MPEG-2');
164
+$Resolutions = array('SD', '480p', '720p', '1080i', '1080p', '4K', 'Other');
165
+$AudioFormats = array('MP3', 'OGG', 'OGG 5.1', 'AAC', 'AAC 5.1', 'AC3', 'AC3 5.1', 'DTS 2.0', 'DTS 5.1', 'DTS-ES 6.1', 'FLAC 2.0', 'FLAC 5.1', 'FLAC 6.1', 'PCM 2.0', 'PCM 5.1', 'PCM 6.1', 'WMA', 'Real Audio', 'DTS-HD', 'DTS-HD MA');
166
+$Subbing = array('Softsubs', 'Hardsubs', 'RAW');
167
+$Languages = array('English', 'Japanese', 'Dual Language');
168
+$Archives = array('7z', 'zip', 'rar');
169
+$ArchivesManga = array('cbz', 'cbr', 'cb7');
170
+
171
+$Formats = array('MP3', 'FLAC', 'Ogg Vorbis', 'AAC', 'AC3', 'DTS');
172
+$Bitrates = array('192', 'APS (VBR)', 'V2 (VBR)', 'V1 (VBR)', '256', 'APX (VBR)', 'V0 (VBR)', 'q8.x (VBR)', '320', 'Lossless', '24bit Lossless', 'Other');
173
+
174
+$CollageCats = array(0=>'Personal', 1=>'Theme', 2=>'Staff picks', 3=>'Artists');
175
+
176
+$ReleaseTypes = array(1=>'Album', 3=>'Soundtrack', 5=>'EP', 6=>'Anthology', 7=>'Compilation', 9=>'Single', 11=>'Live album', 13=>'Remix', 14=>'Bootleg', 15=>'Interview', 16=>'Mixtape', 21=>'Unknown');
177
+//$ForumCats = array(1=>'Site', 5=>'Community', 10=>'Help', 8=>'Music', 20=>'Trash'); //No longer needed
178
+
179
+$ZIPGroups = array(
180
+	0 => 'MP3 (VBR) - High Quality',
181
+	1 => 'MP3 (VBR) - Low Quality',
182
+	2 => 'MP3 (CBR)',
183
+	3 => 'FLAC - Lossless',
184
+	4 => 'Others'
185
+);
186
+
187
+//3D array of attributes, OptionGroup, OptionNumber, Name
188
+$ZIPOptions = array(
189
+	'00' => array(0, 0, 'V0'),
190
+	'01' => array(0, 1, 'APX'),
191
+	'02' => array(0, 2, '256'),
192
+	'03' => array(0, 3, 'V1'),
193
+	'10' => array(1, 0, '224'),
194
+	'11' => array(1, 1, 'V2'),
195
+	'12' => array(1, 2, 'APS'),
196
+	'13' => array(1, 3, '192'),
197
+	'20' => array(2, 0, '320'),
198
+	'21' => array(2, 1, '256'),
199
+	'22' => array(2, 2, '224'),
200
+	'23' => array(2, 3, '192'),
201
+	'30' => array(3, 0, 'FLAC / 24bit / Vinyl'),
202
+	'31' => array(3, 1, 'FLAC / 24bit / DVD'),
203
+	'32' => array(3, 2, 'FLAC / 24bit / SACD'),
204
+	'33' => array(3, 3, 'FLAC / Log (100) / Cue'),
205
+	'34' => array(3, 4, 'FLAC / Log (100)'),
206
+	'35' => array(3, 5, 'FLAC / Log'),
207
+	'36' => array(3, 6, 'FLAC'),
208
+	'40' => array(4, 0, 'DTS'),
209
+	'41' => array(4, 1, 'Ogg Vorbis'),
210
+	'42' => array(4, 2, 'AAC - 320'),
211
+	'43' => array(4, 3, 'AAC - 256'),
212
+	'44' => array(4, 4, 'AAC - q5.5'),
213
+	'45' => array(4, 5, 'AAC - q5'),
214
+	'46' => array(4, 6, 'AAC - 192')
215
+);
216
+
217
+// Ratio requirements, in descending order
218
+// Columns: Download amount, required ratio, grace period
219
+$RatioRequirements = array(
220
+	//array(0, 0, date('Y-m-d H:i:s'))
221
+	array(200 * 1024 * 1024 * 1024, 0.60, date('Y-m-d H:i:s')),
222
+	array(160 * 1024 * 1024 * 1024, 0.60, date('Y-m-d H:i:s')),
223
+	array(120 * 1024 * 1024 * 1024, 0.50, date('Y-m-d H:i:s')),
224
+	array(100 * 1024 * 1024 * 1024, 0.40, date('Y-m-d H:i:s')),
225
+	array(80  * 1024 * 1024 * 1024, 0.30, date('Y-m-d H:i:s')),
226
+	array(60  * 1024 * 1024 * 1024, 0.20, date('Y-m-d H:i:s')),
227
+	array(40  * 1024 * 1024 * 1024, 0.15, date('Y-m-d H:i:s')),
228
+	array(20  * 1024 * 1024 * 1024, 0.10, date('Y-m-d H:i:s')),
229
+	array(10  * 1024 * 1024 * 1024, 0.05, date('Y-m-d H:i:s', time() - (60 * 60 * 24 * 14)))
230
+);
231
+
232
+//Captcha fonts should be located in /classes/fonts
233
+$CaptchaFonts = array(
234
+		'ARIBLK.TTF',
235
+		'IMPACT.TTF',
236
+		'TREBUC.TTF',
237
+		'TREBUCBD.TTF',
238
+		'TREBUCBI.TTF',
239
+		'TREBUCIT.TTF',
240
+		'VERDANA.TTF',
241
+		'VERDANAB.TTF',
242
+		'VERDANAI.TTF',
243
+		'VERDANAZ.TTF');
244
+//Captcha images should be located in /captcha
245
+$CaptchaBGs = array(
246
+		'captcha1.png',
247
+		'captcha2.png',
248
+		'captcha3.png',
249
+		'captcha4.png',
250
+		'captcha5.png',
251
+		'captcha6.png',
252
+		'captcha7.png',
253
+		'captcha8.png',
254
+		'captcha9.png');
255
+
256
+// Special characters, and what they should be converted to
257
+// Used for torrent searching
258
+$SpecialChars = array(
259
+		'&' => 'and'
260
+);
261
+
262
+// God I wish I didn't have to do this but I just don't care anymore.
263
+$AutomatedBadgeIDs = array(
264
+	'DL' => array(
265
+		'8' => 10,
266
+		'16' => 11,
267
+		'32' => 12,
268
+		'64' => 13,
269
+		'128' => 14,
270
+		'256' => 15,
271
+		'512' => 16,
272
+		'1024' => 17,
273
+		'2048' => 18
274
+	)
275
+);
276
+?>

+ 48
- 0
classes/cookie.class.php View File

1
+<?
2
+/*************************************************************************|
3
+|--------------- Cookie class --------------------------------------------|
4
+|*************************************************************************|
5
+
6
+This class handles cookies.
7
+
8
+$Cookie->get(); is user provided and untrustworthy
9
+
10
+|*************************************************************************/
11
+
12
+/*
13
+interface COOKIE_INTERFACE {
14
+	public function get($Key);
15
+	public function set($Key, $Value, $Seconds, $LimitAccess);
16
+	public function del($Key);
17
+
18
+	public function flush();
19
+}
20
+*/
21
+
22
+class COOKIE /*implements COOKIE_INTERFACE*/ {
23
+	const LIMIT_ACCESS = true; //If true, blocks JS cookie API access by default (can be overridden case by case)
24
+	const PREFIX = ''; //In some cases you may desire to prefix your cookies
25
+
26
+	public function get($Key) {
27
+		if (!isset($_COOKIE[SELF::PREFIX.$Key])) {
28
+			return false;
29
+		}
30
+		return $_COOKIE[SELF::PREFIX.$Key];
31
+	}
32
+
33
+	//Pass the 4th optional param as false to allow JS access to the cookie
34
+	public function set($Key, $Value, $Seconds = 86400, $LimitAccess = SELF::LIMIT_ACCESS) {
35
+		setcookie(SELF::PREFIX.$Key, $Value, time() + $Seconds, '/', SITE_URL, $_SERVER['SERVER_PORT'] === '443', $LimitAccess, false);
36
+	}
37
+
38
+	public function del($Key) {
39
+		setcookie(SELF::PREFIX.$Key, '', time() - 24 * 3600); //3600 vs 1 second to account for potential clock desyncs
40
+	}
41
+
42
+	public function flush() {
43
+		$Cookies = array_keys($_COOKIE);
44
+		foreach ($Cookies as $Cookie) {
45
+			$this->del($Cookie);
46
+		}
47
+	}
48
+}

+ 37
- 0
classes/dbcrypt.class.php View File

1
+<?
2
+class DBCrypt {
3
+  /**
4
+   * Encrypts input text for use in database
5
+   *
6
+   * @param string $plaintext
7
+   * @return encrypted string or false if DB key not accessible
8
+   */
9
+	public static function encrypt($plaintext) {
10
+		if (apc_exists('DBKEY')) {
11
+			$iv_size = openssl_cipher_iv_length('AES-128-CBC');
12
+			$iv = openssl_random_pseudo_bytes($iv_size);
13
+			$ret =  base64_encode($iv.openssl_encrypt($plaintext, 'AES-128-CBC', apc_fetch('DBKEY'), OPENSSL_RAW_DATA, $iv));
14
+			return $ret;
15
+		} else {
16
+			return false;
17
+		}
18
+	}
19
+
20
+  /**
21
+   * Decrypts input text from database
22
+   *
23
+   * @param string $ciphertext
24
+   * @return decrypted string string or false if DB key not accessible
25
+   */
26
+	public static function decrypt($ciphertext) {
27
+		if (apc_exists('DBKEY')) {
28
+			$iv_size = openssl_cipher_iv_length('AES-128-CBC');
29
+			$iv = substr(base64_decode($ciphertext), 0, $iv_size);
30
+			$ciphertext = substr(base64_decode($ciphertext), $iv_size);
31
+			return openssl_decrypt($ciphertext, 'AES-128-CBC', apc_fetch('DBKEY'), OPENSSL_RAW_DATA, $iv);
32
+		} else {
33
+			return false;
34
+		}	
35
+	}
36
+}
37
+?>

+ 675
- 0
classes/debug.class.php View File

1
+<?
2
+// Debug info for developers
3
+ini_set('max_execution_time',600);
4
+define('MAX_TIME', 20000); //Maximum execution time in ms
5
+define('MAX_ERRORS', 0); //Maxmimum errors, warnings, notices we will allow in a page
6
+define('MAX_MEMORY', 80 * 1024 * 1024); //Maximum memory used per pageload
7
+define('MAX_QUERIES', 30); //Maxmimum queries
8
+
9
+class DEBUG {
10
+	public $Errors = array();
11
+	public $Flags = array();
12
+	public $Perf = array();
13
+	private $LoggedVars = array();
14
+
15
+	public function profile($Automatic = '') {
16
+		global $ScriptStartTime;
17
+		$Reason = array();
18
+
19
+		if (!empty($Automatic)) {
20
+			$Reason[] = $Automatic;
21
+		}
22
+
23
+		$Micro = (microtime(true) - $ScriptStartTime) * 1000;
24
+		if ($Micro > MAX_TIME && !defined('TIME_EXCEPTION')) {
25
+			$Reason[] = number_format($Micro, 3).' ms';
26
+		}
27
+
28
+		$Errors = count($this->get_errors());
29
+		if ($Errors > MAX_ERRORS && !defined('ERROR_EXCEPTION')) {
30
+			$Reason[] = $Errors.' PHP errors';
31
+		}
32
+		/*
33
+		$Queries = count($this->get_queries());
34
+		if ($Queries > MAX_QUERIES && !defined('QUERY_EXCEPTION')) {
35
+			$Reason[] = $Queries.' Queries';
36
+		}
37
+		*/
38
+		$Ram = memory_get_usage(true);
39
+		if ($Ram > MAX_MEMORY && !defined('MEMORY_EXCEPTION')) {
40
+			$Reason[] = Format::get_size($Ram).' RAM used';
41
+		}
42
+
43
+		G::$DB->warnings(); // see comment in MYSQL::query
44
+		/*$Queries = $this->get_queries();
45
+		$DBWarningCount = 0;
46
+		foreach ($Queries as $Query) {
47
+			if (!empty($Query[2])) {
48
+				$DBWarningCount += count($Query[2]);
49
+			}
50
+		}
51
+		if ($DBWarningCount) {
52
+			$Reason[] = $DBWarningCount . ' DB warning(s)';
53
+		}*/
54
+
55
+		$CacheStatus = G::$Cache->server_status();
56
+		if (in_array(0, $CacheStatus) && !G::$Cache->get_value('cache_fail_reported')) {
57
+			// Limit to max one report every 15 minutes to avoid massive debug spam
58
+			G::$Cache->cache_value('cache_fail_reported', true, 900);
59
+			$Reason[] = "Cache server error";
60
+		}
61
+
62
+		if (isset($_REQUEST['profile'])) {
63
+			$Reason[] = 'Requested by ' . G::$LoggedUser['Username'];
64
+		}
65
+
66
+		$this->Perf['Memory usage'] = (($Ram>>10) / 1024).' MB';
67
+		$this->Perf['Page process time'] = number_format($Micro / 1000, 3).' s';
68
+		$this->Perf['CPU time'] = number_format($this->get_cpu_time() / 1000000, 3).' s';
69
+
70
+		if (isset($Reason[0])) {
71
+			$this->log_var($CacheStatus, 'Cache server status');
72
+			$this->analysis(implode(', ', $Reason));
73
+			return true;
74
+		}
75
+
76
+		return false;
77
+	}
78
+
79
+	public function analysis($Message, $Report = '', $Time = 43200) {
80
+		global $Document;
81
+		if (empty($Report)) {
82
+			$Report = $Message;
83
+		}
84
+		$Identifier = Users::make_secret(5);
85
+		G::$Cache->cache_value(
86
+			'analysis_'.$Identifier,
87
+			array(
88
+				'url' => $_SERVER['REQUEST_URI'],
89
+				'message' => $Report,
90
+				'errors' => $this->get_errors(true),
91
+				'queries' => $this->get_queries(),
92
+				'flags' => $this->get_flags(),
93
+				'includes' => $this->get_includes(),
94
+				'cache' => $this->get_cache_keys(),
95
+				'vars' => $this->get_logged_vars(),
96
+				'perf' => $this->get_perf(),
97
+				'ocelot' => $this->get_ocelot_requests()
98
+			),
99
+			$Time
100
+		);
101
+		$RequestURI = !empty($_SERVER['REQUEST_URI']) ? substr($_SERVER['REQUEST_URI'], 1) : '';
102
+		send_irc('PRIVMSG '.LAB_CHAN." :{$Message} $Document ".site_url()."tools.php?action=analysis&case=$Identifier ".site_url().$RequestURI);
103
+	}
104
+
105
+	public function get_cpu_time() {
106
+		if (!defined('PHP_WINDOWS_VERSION_MAJOR')) {
107
+			global $CPUTimeStart;
108
+			$RUsage = getrusage();
109
+			$CPUTime = $RUsage['ru_utime.tv_sec'] * 1000000 + $RUsage['ru_utime.tv_usec'] - $CPUTimeStart;
110
+			return $CPUTime;
111
+		}
112
+		return false;
113
+	}
114
+
115
+	public function log_var($Var, $VarName = false) {
116
+		$BackTrace = debug_backtrace();
117
+		$ID = Users::make_secret(5);
118
+		if (!$VarName) {
119
+			$VarName = $ID;
120
+		}
121
+		$File = array('path' => substr($BackTrace[0]['file'], strlen(SERVER_ROOT)), 'line' => $BackTrace[0]['line']);
122
+		$this->LoggedVars[$ID] = array($VarName => array('bt' => $File, 'data' => $Var));
123
+	}
124
+
125
+	public function set_flag($Event) {
126
+		global $ScriptStartTime;
127
+		$this->Flags[] = array($Event, (microtime(true) - $ScriptStartTime) * 1000, memory_get_usage(true), $this->get_cpu_time());
128
+	}
129
+
130
+	//This isn't in the constructor because $this is not available, and the function cannot be made static
131
+	public function handle_errors() {
132
+		//error_reporting(E_ALL ^ E_STRICT | E_WARNING | E_DEPRECATED | E_ERROR | E_PARSE); //E_STRICT disabled
133
+		error_reporting(E_WARNING | E_ERROR | E_PARSE);
134
+		set_error_handler(array($this, 'php_error_handler'));
135
+	}
136
+
137
+	protected function format_args($Array) {
138
+		$LastKey = -1;
139
+		$Return = array();
140
+		foreach ($Array as $Key => $Val) {
141
+			$Return[$Key] = '';
142
+			if (!is_int($Key) || $Key != $LastKey + 1) {
143
+				$Return[$Key] .= "'$Key' => ";
144
+			}
145
+				if ($Val === true) {
146
+					$Return[$Key] .= 'true';
147
+				} elseif ($Val === false) {
148
+					$Return[$Key] .= 'false';
149
+				} elseif (is_string($Val)) {
150
+					$Return[$Key] .= "'$Val'";
151
+				} elseif (is_int($Val)) {
152
+					$Return[$Key] .= $Val;
153
+				} elseif (is_object($Val)) {
154
+					$Return[$Key] .= get_class($Val);
155
+				} elseif (is_array($Val)) {
156
+					$Return[$Key] .= 'array('.$this->format_args($Val).')';
157
+				}
158
+			$LastKey = $Key;
159
+		}
160
+		return implode(', ', $Return);
161
+	}
162
+
163
+	public function php_error_handler($Level, $Error, $File, $Line) {
164
+		//Who added this, it's still something to pay attention to...
165
+		if (stripos('Undefined index', $Error) !== false) {
166
+			//return true;
167
+		}
168
+
169
+		$Steps = 1; //Steps to go up in backtrace, default one
170
+		$Call = '';
171
+		$Args = '';
172
+		$Tracer = debug_backtrace();
173
+
174
+		//This is in case something in this function goes wrong and we get stuck with an infinite loop
175
+		if (isset($Tracer[$Steps]['function'], $Tracer[$Steps]['class']) && $Tracer[$Steps]['function'] == 'php_error_handler' && $Tracer[$Steps]['class'] == 'DEBUG') {
176
+			return true;
177
+		}
178
+
179
+		//If this error was thrown, we return the function which threw it
180
+		if (isset($Tracer[$Steps]['function']) && $Tracer[$Steps]['function'] == 'trigger_error') {
181
+			$Steps++;
182
+			$File = $Tracer[$Steps]['file'];
183
+			$Line = $Tracer[$Steps]['line'];
184
+		}
185
+
186
+		//At this time ONLY Array strict typing is fully supported.
187
+		//Allow us to abuse strict typing (IE: function test(Array))
188
+		if (preg_match('/^Argument (\d+) passed to \S+ must be an (array), (array|string|integer|double|object) given, called in (\S+) on line (\d+) and defined$/', $Error, $Matches)) {
189
+			$Error = 'Type hinting failed on arg '.$Matches[1]. ', expected '.$Matches[2].' but found '.$Matches[3];
190
+			$File = $Matches[4];
191
+			$Line = $Matches[5];
192
+		}
193
+
194
+		//Lets not be repetative
195
+		if (($Tracer[$Steps]['function'] == 'include' || $Tracer[$Steps]['function'] == 'require' ) && isset($Tracer[$Steps]['args'][0]) && $Tracer[$Steps]['args'][0] == $File) {
196
+			unset($Tracer[$Steps]['args']);
197
+		}
198
+
199
+		//Class
200
+		if (isset($Tracer[$Steps]['class'])) {
201
+			$Call .= $Tracer[$Steps]['class'].'::';
202
+		}
203
+
204
+		//Function & args
205
+		if (isset($Tracer[$Steps]['function'])) {
206
+			$Call .= $Tracer[$Steps]['function'];
207
+			if (isset($Tracer[$Steps]['args'][0])) {
208
+				$Args = $this->format_args($Tracer[$Steps]['args']);
209
+			}
210
+		}
211
+
212
+		//Shorten the path & we're done
213
+		$File = str_replace(SERVER_ROOT, '', $File);
214
+		$Error = str_replace(SERVER_ROOT, '', $Error);
215
+
216
+		if (DEBUG_WARNINGS) {
217
+			$this->Errors[] = array($Error, $File.':'.$Line, $Call, $Args);
218
+		}
219
+		return true;
220
+	}
221
+
222
+	/* Data wrappers */
223
+
224
+	public function get_perf() {
225
+		if (empty($this->Perf)) {
226
+			global $ScriptStartTime;
227
+			$PageTime = (microtime(true) - $ScriptStartTime);
228
+			$CPUTime = $this->get_cpu_time();
229
+			$Perf = array(
230
+				'Memory usage' => Format::get_size(memory_get_usage(true)),
231
+				'Page process time' => number_format($PageTime, 3).' s');
232
+			if ($CPUTime) {
233
+				$Perf['CPU time'] = number_format($CPUTime / 1000000, 3).' s';
234
+			}
235
+			return $Perf;
236
+		}
237
+		return $this->Perf;
238
+	}
239
+
240
+	public function get_flags() {
241
+		return $this->Flags;
242
+	}
243
+
244
+	public function get_errors($Light = false) {
245
+		//Because the cache can't take some of these variables
246
+		if ($Light) {
247
+			foreach ($this->Errors as $Key => $Value) {
248
+				$this->Errors[$Key][3] = '';
249
+			}
250
+		}
251
+		return $this->Errors;
252
+	}
253
+
254
+	public function get_constants() {
255
+		return get_defined_constants(true);
256
+	}
257
+
258
+	public function get_classes() {
259
+		foreach (get_declared_classes() as $Class) {
260
+			$Classes[$Class]['Vars'] = get_class_vars($Class);
261
+			$Classes[$Class]['Functions'] = get_class_methods($Class);
262
+		}
263
+		return $Classes;
264
+	}
265
+
266
+	public function get_extensions() {
267
+		foreach (get_loaded_extensions() as $Extension) {
268
+			$Extensions[$Extension]['Functions'] = get_extension_funcs($Extension);
269
+		}
270
+		return $Extensions;
271
+	}
272
+
273
+	public function get_includes() {
274
+		return get_included_files();
275
+	}
276
+
277
+	public function get_cache_time() {
278
+		return G::$Cache->Time;
279
+	}
280
+
281
+	public function get_cache_keys() {
282
+		return array_keys(G::$Cache->CacheHits);
283
+	}
284
+
285
+	public function get_sphinxql_queries() {
286
+		if (class_exists('Sphinxql')) {
287
+			return Sphinxql::$Queries;
288
+		}
289
+	}
290
+
291
+	public function get_sphinxql_time() {
292
+		if (class_exists('Sphinxql')) {
293
+			return Sphinxql::$Time;
294
+		}
295
+	}
296
+
297
+	public function get_queries() {
298
+		return G::$DB->Queries;
299
+	}
300
+
301
+	public function get_query_time() {
302
+		return G::$DB->Time;
303
+	}
304
+
305
+	public function get_logged_vars() {
306
+		return $this->LoggedVars;
307
+	}
308
+
309
+	public function get_ocelot_requests() {
310
+		if (class_exists('Tracker')) {
311
+			return Tracker::$Requests;
312
+		}
313
+	}
314
+
315
+	/* Output Formatting */
316
+
317
+	public function perf_table($Perf = false) {
318
+		if (!is_array($Perf)) {
319
+			$Perf = $this->get_perf();
320
+		}
321
+		if (empty($Perf)) {
322
+			return;
323
+		}
324
+?>
325
+	<table class="layout" width="100%">
326
+		<tr>
327
+			<td align="left"><strong><a href="#" onclick="$(this).parents('.layout').next('#debug_perf').gtoggle(); return false;" class="brackets">View</a> Performance Statistics:</strong></td>
328
+		</tr>
329
+	</table>
330
+	<table id="debug_perf" class="debug_table hidden" width="100%">
331
+<?
332
+		foreach ($Perf as $Stat => $Value) {
333
+?>
334
+		<tr valign="top">
335
+			<td class="debug_perf_stat"><?=$Stat?></td>
336
+			<td class="debug_perf_data"><?=$Value?></td>
337
+		</tr>
338
+<?
339
+		}
340
+?>
341
+	</table>
342
+<?
343
+	}
344
+
345
+	public function include_table($Includes = false) {
346
+		if (!is_array($Includes)) {
347
+			$Includes = $this->get_includes();
348
+		}
349
+?>
350
+	<table class="layout" width="100%">
351
+		<tr>
352
+			<td align="left"><strong><a href="#" onclick="$(this).parents('.layout').next('#debug_include').gtoggle(); return false;" class="brackets">View</a> <?=number_format(count($Includes))?> Includes:</strong></td>
353
+		</tr>
354
+	</table>
355
+	<table id="debug_include" class="debug_table hidden" width="100%">
356
+<?
357
+		foreach ($Includes as $File) {
358
+?>
359
+		<tr valign="top">
360
+			<td><?=$File?></td>
361
+		</tr>
362
+<?
363
+		}
364
+?>
365
+	</table>
366
+<?
367
+	}
368
+
369
+	public function class_table($Classes = false) {
370
+		if (!is_array($Classes)) {
371
+			$Classes = $this->get_classes();
372
+		}
373
+?>
374
+	<table class="layout" width="100%">
375
+		<tr>
376
+			<td align="left"><strong><a href="#" onclick="$(this).parents('.layout').next('#debug_classes').gtoggle(); return false;" class="brackets">View</a> Classes:</strong></td>
377
+		</tr>
378
+	</table>
379
+	<table id="debug_classes" class="debug_table hidden" width="100%">
380
+		<tr>
381
+			<td align="left">
382
+				<pre>
383
+<?					print_r($Classes); echo "\n"; ?>
384
+				</pre>
385
+			</td>
386
+		</tr>
387
+	</table>
388
+<?
389
+	}
390
+
391
+	public function extension_table() {
392
+?>
393
+	<table class="layout" width="100%">
394
+		<tr>
395
+			<td align="left"><strong><a href="#" onclick="$(this).parents('.layout').next('#debug_extensions').gtoggle(); return false;" class="brackets">View</a> Extensions:</strong></td>
396
+		</tr>
397
+	</table>
398
+	<table id="debug_extensions" class="debug_table hidden" width="100%">
399
+		<tr>
400
+			<td align="left">
401
+				<pre>
402
+<?					print_r($this->get_extensions()); echo "\n"; ?>
403
+				</pre>
404
+			</td>
405
+		</tr>
406
+	</table>
407
+<?
408
+	}
409
+
410
+	public function flag_table($Flags = false) {
411
+		if (!is_array($Flags)) {
412
+			$Flags = $this->get_flags();
413
+		}
414
+		if (empty($Flags)) {
415
+			return;
416
+		}
417
+?>
418
+	<table class="layout" width="100%">
419
+		<tr>
420
+			<td align="left"><strong><a href="#" onclick="$(this).parents('.layout').next('#debug_flags').gtoggle(); return false;" class="brackets">View</a> Flags:</strong></td>
421
+		</tr>
422
+	</table>
423
+	<table id="debug_flags" class="debug_table hidden" width="100%">
424
+		<tr valign="top">
425
+			<td align="left" class="debug_flags_event"><strong>Event</strong></td>
426
+			<td align="left" class="debug_flags_time"><strong>Page time</strong></td>
427
+<?		if ($Flags[0][3] !== false) { ?>
428
+			<td align="left" class="debug_flags_time"><strong>CPU time</strong></td>
429
+<?		} ?>
430
+			<td align="left" class="debug_flags_memory"><strong>Memory</strong></td>
431
+		</tr>
432
+<?
433
+		foreach ($Flags as $Flag) {
434
+			list($Event, $MicroTime, $Memory, $CPUTime) = $Flag;
435
+?>
436
+		<tr valign="top">
437
+			<td align="left"><?=$Event?></td>
438
+			<td align="left"><?=number_format($MicroTime, 3)?> ms</td>
439
+<?			if ($CPUTime !== false) { ?>
440
+			<td align="left"><?=number_format($CPUTime / 1000, 3)?> ms</td>
441
+<?			} ?>
442
+			<td align="left"><?=Format::get_size($Memory)?></td>
443
+		</tr>
444
+<?		} ?>
445
+	</table>
446
+<?
447
+	}
448
+
449
+	public function constant_table($Constants = false) {
450
+		if (!is_array($Constants)) {
451
+			$Constants = $this->get_constants();
452
+		}
453
+?>
454
+	<table class="layout" width="100%">
455
+		<tr>
456
+			<td align="left"><strong><a href="#" onclick="$(this).parents('.layout').next('#debug_constants').gtoggle(); return false;" class="brackets">View</a> Constants:</strong></td>
457
+		</tr>
458
+	</table>
459
+	<table id="debug_constants" class="debug_table hidden" width="100%">
460
+		<tr>
461
+			<td align="left" class="debug_data debug_constants_data">
462
+				<pre>
463
+<?=					display_str(print_r($Constants, true))?>
464
+				</pre>
465
+			</td>
466
+		</tr>
467
+	</table>
468
+<?
469
+	}
470
+
471
+	public function ocelot_table($OcelotRequests = false) {
472
+		if (!is_array($OcelotRequests)) {
473
+			$OcelotRequests = $this->get_ocelot_requests();
474
+		}
475
+		if (empty($OcelotRequests)) {
476
+			return;
477
+		}
478
+?>
479
+	<table class="layout" width="100%">
480
+		<tr>
481
+			<td align="left"><strong><a href="#" onclick="$('#debug_ocelot').gtoggle(); return false;" class="brackets">View</a> <?=number_format(count($OcelotRequests))?> Ocelot requests:</strong></td>
482
+		</tr>
483
+	</table>
484
+	<table id="debug_ocelot" class="debug_table hidden" width="100%">
485
+<?		foreach ($OcelotRequests as $i => $Request) { ?>
486
+		<tr>
487
+			<td align="left" class="debug_data debug_ocelot_data">
488
+				<a href="#" onclick="$('#debug_ocelot_<?=$i?>').gtoggle(); return false"><?=display_str($Request['path'])?></a>
489
+				<pre id="debug_ocelot_<?=$i?>" class="hidden"><?=display_str($Request['response'])?></pre>
490
+			</td>
491
+			<td align="left" class="debug_info" style="width: 100px;">
492
+				<?=display_str($Request['status'])?>
493
+			</td>
494
+			<td align="left" class="debug_info debug_timing" style="width: 100px;">
495
+				<?=number_format($Request['time'], 5)?> ms
496
+			</td>
497
+		</tr>
498
+<?		} ?>
499
+	</table>
500
+<?
501
+	}
502
+
503
+	public function cache_table($CacheKeys = false) {
504
+		$Header = 'Cache Keys';
505
+		if (!is_array($CacheKeys)) {
506
+			$CacheKeys = $this->get_cache_keys();
507
+			$Header .= ' ('.number_format($this->get_cache_time(), 5).' ms)';
508
+		}
509
+		if (empty($CacheKeys)) {
510
+			return;
511
+		}
512
+		$Header = ' '.number_format(count($CacheKeys))." $Header:";
513
+
514
+?>
515
+	<table class="layout" width="100%">
516
+		<tr>
517
+			<td align="left"><strong><a href="#" onclick="$(this).parents('.layout').next('#debug_cache').gtoggle(); return false;" class="brackets">View</a><?=$Header?></strong></td>
518
+		</tr>
519
+	</table>
520
+	<table id="debug_cache" class="debug_table hidden" width="100%">
521
+<? 		foreach ($CacheKeys as $Key) { ?>
522
+		<tr>
523
+			<td class="label nobr debug_info debug_cache_key">
524
+				<a href="#" onclick="$('#debug_cache_<?=$Key?>').gtoggle(); return false;"><?=display_str($Key)?></a>
525
+				<a href="tools.php?action=clear_cache&amp;key=<?=$Key?>&amp;type=clear" target="_blank" class="brackets tooltip" title="Clear this cache key">Clear</a>
526
+			</td>
527
+			<td align="left" class="debug_data debug_cache_data">
528
+				<pre id="debug_cache_<?=$Key?>" class="hidden">
529
+<?=					display_str(print_r(G::$Cache->get_value($Key, true), true))?>
530
+				</pre>
531
+			</td>
532
+		</tr>
533
+<?		} ?>
534
+	</table>
535
+<?
536
+	}
537
+
538
+	public function error_table($Errors = false) {
539
+		if (!is_array($Errors)) {
540
+			$Errors = $this->get_errors();
541
+		}
542
+		if (empty($Errors)) {
543
+			return;
544
+		}
545
+?>
546
+	<table class="layout" width="100%">
547
+		<tr>
548
+			<td align="left"><strong><a href="#" onclick="$(this).parents('.layout').next('#debug_error').gtoggle(); return false;" class="brackets">View</a> <?=number_format(count($Errors))?> Errors:</strong></td>
549
+		</tr>
550
+	</table>
551
+	<table id="debug_error" class="debug_table hidden" width="100%">
552
+<?
553
+		foreach ($Errors as $Error) {
554
+			list($Error, $Location, $Call, $Args) = $Error;
555
+?>
556
+		<tr valign="top">
557
+			<td align="left" class="debug_info debug_error_call">
558
+				<?=display_str($Call)?>(<?=display_str($Args)?>)
559
+			</td>
560
+			<td class="debug_data debug_error_data" align="left">
561
+				<?=display_str($Error)?>
562
+			</td>
563
+			<td align="left">
564
+				<?=display_str($Location)?>
565
+			</td>
566
+		</tr>
567
+<?		} ?>
568
+	</table>
569
+<?
570
+	}
571
+
572
+	public function query_table($Queries=false) {
573
+		$Header = 'Queries';
574
+		if (!is_array($Queries)) {
575
+			$Queries = $this->get_queries();
576
+			$Header .= ' ('.number_format($this->get_query_time(), 5).' ms)';
577
+		}
578
+		if (empty($Queries)) {
579
+			return;
580
+		}
581
+		$Header = ' '.number_format(count($Queries))." $Header:";
582
+?>
583
+	<table class="layout" width="100%">
584
+		<tr>
585
+			<td align="left"><strong><a href="#" onclick="$(this).parents('.layout').next('#debug_database').gtoggle(); return false;" class="brackets">View</a><?=$Header?></strong></td>
586
+		</tr>
587
+	</table>
588
+	<table id="debug_database" class="debug_table hidden" width="100%">
589
+<?
590
+		foreach ($Queries as $Query) {
591
+			$SQL = $Query[0] ?? null;
592
+			$Time = $Query[1] ?? null;
593
+			$Warnings = $Query[2] ?? null;
594
+			if ($Warnings !== null) {
595
+				$Warnings = implode('<br />', $Warnings);
596
+			}
597
+?>
598
+		<tr valign="top">
599
+			<td class="debug_data debug_query_data"><div><?=str_replace("\t", '&nbsp;&nbsp;', nl2br(display_str(trim($SQL))))?></div></td>
600
+			<td class="debug_info debug_query_time" style="width: 130px;" align="left"><?=number_format($Time, 5)?> ms</td>
601
+			<td class="debug_info debug_query_warnings"><?=$Warnings?></td>
602
+		</tr>
603
+<?		} ?>
604
+	</table>
605
+<?
606
+	}
607
+
608
+	public function sphinx_table($Queries = false) {
609
+		$Header = 'Searches';
610
+		if (!is_array($Queries)) {
611
+			$Queries = $this->get_sphinxql_queries();
612
+			$Header .= ' ('.number_format($this->get_sphinxql_time(), 5).' ms)';
613
+		}
614
+		if (empty($Queries)) {
615
+			return;
616
+		}
617
+		$Header = ' '.number_format(count($Queries))." $Header:";
618
+?>
619
+	<table class="layout" width="100%">
620
+		<tr>
621
+			<td align="left"><strong><a href="#" onclick="$(this).parents('.layout').next('#debug_sphinx').gtoggle(); return false;" class="brackets">View</a><?=$Header?></strong></td>
622
+		</tr>
623
+	</table>
624
+	<table id="debug_sphinx" class="debug_table hidden" width="100%">
625
+<?
626
+		foreach ($Queries as $Query) {
627
+			list($Params, $Time) = $Query;
628
+?>
629
+		<tr valign="top">
630
+			<td class="debug_data debug_sphinx_data"><pre><?=str_replace("\t", '	', $Params)?></pre></td>
631
+			<td class="debug_info debug_sphinx_time" style="width: 130px;" align="left"><?=number_format($Time, 5)?> ms</td>
632
+		</tr>
633
+<?		} ?>
634
+	</table>
635
+<?
636
+	}
637
+
638
+	public function vars_table($Vars = false) {
639
+		$Header = 'Logged Variables';
640
+		if (empty($Vars)) {
641
+			if (empty($this->LoggedVars)) {
642
+				return;
643
+			}
644
+			$Vars = $this->LoggedVars;
645
+		}
646
+		$Header = ' '.number_format(count($Vars))." $Header:";
647
+
648
+?>
649
+	<table class="layout" width="100%">
650
+		<tr>
651
+			<td align="left"><strong><a href="#" onclick="$(this).parents('.layout').next('#debug_loggedvars').gtoggle(); return false;" class="brackets">View</a><?=$Header?></strong></td>
652
+		</tr>
653
+	</table>
654
+	<table id="debug_loggedvars" class="debug_table hidden" width="100%">
655
+<?
656
+		foreach ($Vars as $ID => $Var) {
657
+			list($Key, $Data) = each($Var);
658
+			$Size = count($Data['data']);
659
+?>
660
+		<tr>
661
+			<td align="left" class="debug_info debug_loggedvars_name">
662
+				<a href="#" onclick="$('#debug_loggedvars_<?=$ID?>').gtoggle(); return false;"><?=display_str($Key)?></a> (<?=$Size . ($Size == 1 ? ' element' : ' elements')?>)
663
+				<div><?=$Data['bt']['path'].':'.$Data['bt']['line'];?></div>
664
+			</td>
665
+			<td class="debug_data debug_loggedvars_data" align="left">
666
+				<pre id="debug_loggedvars_<?=$ID?>" class="hidden">
667
+<?=					display_str(print_r($Data['data'], true))?>
668
+				</pre>
669
+			</td>
670
+		</tr>
671
+<?		} ?>
672
+	</table>
673
+<?
674
+	}
675
+}

+ 948
- 0
classes/donations.class.php View File

1
+<?
2
+
3
+define('BTC_API_URL', 'https://api.bitcoinaverage.com/ticker/global/EUR/');
4
+define('USD_API_URL', 'http://www.google.com/ig/calculator?hl=en&q=1USD=?EUR');
5
+
6
+class Donations {
7
+	private static $ForumDescriptions = array(
8
+		"I want only two houses, rather than seven... I feel like letting go of things",
9
+		"A billion here, a billion there, sooner or later it adds up to real money.",
10
+		"I've cut back, because I'm buying a house in the West Village.",
11
+		"Some girls are just born with glitter in their veins.",
12
+		"I get half a million just to show up at parties. My life is, like, really, really fun.",
13
+		"Some people change when they think they're a star or something",
14
+		"I'd rather not talk about money. It’s kind of gross.",
15
+		"I have not been to my house in Bermuda for two or three years, and the same goes for my house in Portofino. How long do I have to keep leading this life of sacrifice?",
16
+		"When I see someone who is making anywhere from $300,000 to $750,000 a year, that's middle class.",
17
+		"Money doesn't make you happy. I now have $50 million but I was just as happy when I had $48 million.",
18
+		"I'd rather smoke crack than eat cheese from a tin.",
19
+		"I am who I am. I can’t pretend to be somebody who makes $25,000 a year.",
20
+		"A girl never knows when she might need a couple of diamonds at ten 'o' clock in the morning.",
21
+		"I wouldn't run for president. I wouldn't want to move to a smaller house.",
22
+		"I have the stardom glow.",
23
+		"What's Walmart? Do they like, sell wall stuff?",
24
+		"Whenever I watch TV and see those poor starving kids all over the world, I can't help but cry. I mean I'd love to be skinny like that, but not with all those flies and death and stuff.",
25
+		"Too much money ain't enough money.",
26
+		"What's a soup kitchen?",
27
+		"I work very hard and I’m worth every cent!",
28
+		"To all my Barbies out there who date Benjamin Franklin, George Washington, Abraham Lincoln, you'll be better off in life. Get that money."
29
+		);
30
+
31
+	private static $IsSchedule = false;
32
+
33
+	public static function regular_donate($UserID, $DonationAmount, $Source, $Reason, $Currency = "EUR") {
34
+		self::donate($UserID, array(
35
+			"Source" => $Source,
36
+			"Price" => $DonationAmount,
37
+			"Currency" => $Currency,
38
+			"Source" => $Source,
39
+			"Reason" => $Reason,
40
+			"SendPM" => true));
41
+	}
42
+
43
+	public static function donate($UserID, $Args) {
44
+		$UserID = (int)$UserID;
45
+		$QueryID = G::$DB->get_query_id();
46
+
47
+		G::$DB->query("
48
+			SELECT 1
49
+			FROM users_main
50
+			WHERE ID = '$UserID'
51
+			LIMIT 1");
52
+		if (G::$DB->has_results()) {
53
+			G::$Cache->InternalCache = false;
54
+			foreach ($Args as &$Arg) {
55
+				$Arg = db_string($Arg);
56
+			}
57
+			extract($Args);
58
+
59
+			// We don't always get a date passed in.
60
+			if (empty($Date)) {
61
+				$Date = sqltime();
62
+			}
63
+
64
+			// Get the ID of the staff member making the edit
65
+			$AddedBy = 0;
66
+			if (!self::$IsSchedule) {
67
+				$AddedBy = G::$LoggedUser['ID'];
68
+			}
69
+
70
+			// Legacy donor, should remove at some point
71
+			G::$DB->query("
72
+				UPDATE users_info
73
+				SET Donor = '1'
74
+				WHERE UserID = '$UserID'");
75
+			// Give them the extra invite
76
+			$ExtraInvite = G::$DB->affected_rows();
77
+
78
+			// A staff member is directly manipulating donor points
79
+			if (isset($Manipulation) && $Manipulation === "Direct") {
80
+				$DonorPoints = $Rank;
81
+				$AdjustedRank = $Rank >= MAX_EXTRA_RANK ? MAX_EXTRA_RANK : $Rank;
82
+				G::$DB->query("
83
+					INSERT INTO users_donor_ranks
84
+						(UserID, Rank, TotalRank, DonationTime, RankExpirationTime)
85
+					VALUES
86
+						('$UserID', '$AdjustedRank', '$TotalRank', '$Date', NOW())
87
+					ON DUPLICATE KEY UPDATE
88
+						Rank = '$AdjustedRank',
89
+						TotalRank = '$TotalRank',
90
+						DonationTime = '$Date',
91
+						RankExpirationTime = NOW()");
92
+			} else {
93
+				// Donations from the store get donor points directly, no need to calculate them
94
+				if ($Source == "Store Parser") {
95
+					$ConvertedPrice = self::currency_exchange($Amount * $Price, $Currency);
96
+				} else {
97
+					$ConvertedPrice = self::currency_exchange($Price, $Currency);
98
+					$DonorPoints = self::calculate_rank($ConvertedPrice);
99
+				}
100
+				$IncreaseRank = $DonorPoints;
101
+
102
+				// Rank is the same thing as DonorPoints
103
+				$CurrentRank = self::get_rank($UserID);
104
+				// A user's donor rank can never exceed MAX_EXTRA_RANK
105
+				// If the amount they donated causes it to overflow, chnage it to MAX_EXTRA_RANK
106
+				// The total rank isn't affected by this, so their original donor point value is added to it
107
+				if (($CurrentRank + $DonorPoints) >= MAX_EXTRA_RANK) {
108
+					$AdjustedRank = MAX_EXTRA_RANK;
109
+				} else {
110
+					$AdjustedRank = $CurrentRank + $DonorPoints;
111
+				}
112
+				G::$DB->query("
113
+					INSERT INTO users_donor_ranks
114
+						(UserID, Rank, TotalRank, DonationTime, RankExpirationTime)
115
+					VALUES
116
+						('$UserID', '$AdjustedRank', '$DonorPoints', '$Date', NOW())
117
+					ON DUPLICATE KEY UPDATE
118
+						Rank = '$AdjustedRank',
119
+						TotalRank = TotalRank + '$DonorPoints',
120
+						DonationTime = '$Date',
121
+						RankExpirationTime = NOW()");
122
+			}
123
+			// Donor cache key is outdated
124
+			G::$Cache->delete_value("donor_info_$UserID");
125
+
126
+			// Get their rank
127
+			$Rank = self::get_rank($UserID);
128
+			$TotalRank = self::get_total_rank($UserID);
129
+
130
+			// Now that their rank and total rank has been set, we can calculate their special rank
131
+			self::calculate_special_rank($UserID);
132
+
133
+			// Hand out invites
134
+			G::$DB->query("
135
+					SELECT InvitesRecievedRank
136
+					FROM users_donor_ranks
137
+					WHERE UserID = '$UserID'");
138
+			list($InvitesRecievedRank) = G::$DB->next_record();
139
+			$AdjustedRank = $Rank >= MAX_RANK ? (MAX_RANK - 1) : $Rank;
140
+			$InviteRank = $AdjustedRank - $InvitesRecievedRank;
141
+			if ($InviteRank > 0) {
142
+				$Invites = $ExtraInvite ? ($InviteRank + 1) : $InviteRank;
143
+				G::$DB->query("
144
+						UPDATE users_main
145
+						SET Invites = Invites + '$Invites'
146
+						WHERE ID = $UserID");
147
+				G::$DB->query("
148
+						UPDATE users_donor_ranks
149
+						SET InvitesRecievedRank = '$AdjustedRank'
150
+						WHERE UserID = '$UserID'");
151
+			}
152
+
153
+			// Send them a thank you PM
154
+			if ($SendPM) {
155
+				$Subject = "Your contribution has been received and credited. Thank you!";
156
+				$Body = self::get_pm_body($Source, $Currency, $Price, $IncreaseRank, $Rank);
157
+				Misc::send_pm($UserID, 0, $Subject, $Body);
158
+			}
159
+
160
+			// Lastly, add this donation to our history
161
+			G::$DB->query("
162
+				INSERT INTO donations
163
+					(UserID, Amount, Source, Reason, Currency, Email, Time, AddedBy, Rank, TotalRank)
164
+				VALUES
165
+					('$UserID', '$ConvertedPrice', '$Source', '$Reason', '$Currency', '', '$Date', '$AddedBy', '$DonorPoints', '$TotalRank')");
166
+
167
+
168
+			// Clear their user cache keys because the users_info values has been modified
169
+			G::$Cache->delete_value("user_info_$UserID");
170
+			G::$Cache->delete_value("user_info_heavy_$UserID");
171
+			G::$Cache->delete_value("donor_info_$UserID");
172
+
173
+		}
174
+		G::$DB->set_query_id($QueryID);
175
+	}
176
+
177
+	private static function calculate_special_rank($UserID) {
178
+		$UserID = (int)$UserID;
179
+		$QueryID = G::$DB->get_query_id();
180
+		// Are they are special?
181
+		G::$DB->query("
182
+			SELECT TotalRank, SpecialRank
183
+			FROM users_donor_ranks
184
+			WHERE UserID = '$UserID'");
185
+		if (G::$DB->has_results()) {
186
+			// Adjust their special rank depending on the total rank.
187
+			list($TotalRank, $SpecialRank) = G::$DB->next_record();
188
+			if ($TotalRank < 10) {
189
+				$SpecialRank = 0;
190
+			}
191
+			if ($SpecialRank < 1 && $TotalRank >= 10) {
192
+				Misc::send_pm($UserID, 0, "You've Reached Special Donor Rank #1! You've Earned: One User Pick. Details Inside.", self::get_special_rank_one_pm());
193
+				$SpecialRank = 1;
194
+			}
195
+			if ($SpecialRank < 2 && $TotalRank >= 20) {
196
+				Misc::send_pm($UserID, 0, "You've Reached Special Donor Rank #2! You've Earned: The Double-Avatar. Details Inside.", self::get_special_rank_two_pm());
197
+				$SpecialRank = 2;
198
+			}
199
+			if ($SpecialRank < 3 && $TotalRank >= 50) {
200
+				Misc::send_pm($UserID, 0, "You've Reached Special Donor Rank #3! You've Earned: Diamond Rank. Details Inside.", self::get_special_rank_three_pm());
201
+				$SpecialRank = 3;
202
+			}
203
+			// Make them special
204
+			G::$DB->query("
205
+				UPDATE users_donor_ranks
206
+				SET SpecialRank = '$SpecialRank'
207
+				WHERE UserID = '$UserID'");
208
+			G::$Cache->delete_value("donor_info_$UserID");
209
+		}
210
+		G::$DB->set_query_id($QueryID);
211
+	}
212
+
213
+	public static function schedule() {
214
+		self::$IsSchedule = true;
215
+
216
+		DonationsBitcoin::find_new_donations();
217
+		self::expire_ranks();
218
+		self::get_new_conversion_rates();
219
+	}
220
+
221
+	public static function expire_ranks() {
222
+		$QueryID = G::$DB->get_query_id();
223
+		G::$DB->query("
224
+			SELECT UserID, Rank
225
+			FROM users_donor_ranks
226
+			WHERE Rank > 1
227
+				AND SpecialRank != 3
228
+				AND RankExpirationTime < NOW() - INTERVAL 766 HOUR");
229
+				// 2 hours less than 32 days to account for schedule run times
230
+
231
+		if (G::$DB->record_count() > 0) {
232
+			$UserIDs = array();
233
+			while (list($UserID, $Rank) = G::$DB->next_record()) {
234
+				G::$Cache->delete_value("donor_info_$UserID");
235
+				G::$Cache->delete_value("donor_title_$UserID");
236
+				G::$Cache->delete_value("donor_profile_rewards_$UserID");
237
+				$UserIDs[] = $UserID;
238
+			}
239
+			$In = implode(',', $UserIDs);
240
+			G::$DB->query("
241
+				UPDATE users_donor_ranks
242
+				SET Rank = Rank - IF(Rank = " . MAX_RANK . ", 2, 1), RankExpirationTime = NOW()
243
+				WHERE UserID IN ($In)");
244
+		}
245
+		G::$DB->set_query_id($QueryID);
246
+	}
247
+
248
+	private static function calculate_rank($Amount) {
249
+		return floor($Amount / 5);
250
+	}
251
+
252
+	public static function update_rank($UserID, $Rank, $TotalRank, $Reason) {
253
+		$Rank = (int)$Rank;
254
+		$TotalRank = (int)$TotalRank;
255
+
256
+		self::donate($UserID, array(
257
+			"Manipulation" => "Direct",
258
+			"Rank" => $Rank,
259
+			"TotalRank" => $TotalRank,
260
+			"Reason" => $Reason,
261
+			"Source" => "Modify Values",
262
+			"Currency" => "EUR"));
263
+	}
264
+
265
+	public static function hide_stats($UserID) {
266
+		$QueryID = G::$DB->get_query_id();
267
+		G::$DB->query("
268
+			INSERT INTO users_donor_ranks
269
+				(UserID, Hidden)
270
+			VALUES
271
+				('$UserID', '1')
272
+			ON DUPLICATE KEY UPDATE
273
+				Hidden = '1'");
274
+		G::$DB->set_query_id($QueryID);
275
+	}
276
+
277
+	public static function show_stats($UserID) {
278
+		$QueryID = G::$DB->get_query_id();
279
+		G::$DB->query("
280
+			INSERT INTO users_donor_ranks
281
+				(UserID, Hidden)
282
+			VALUES
283
+				('$UserID', '0')
284
+			ON DUPLICATE KEY UPDATE
285
+				Hidden = '0'");
286
+		G::$DB->set_query_id($QueryID);
287
+	}
288
+
289
+	public static function is_visible($UserID) {
290
+		$QueryID = G::$DB->get_query_id();
291
+		G::$DB->query("
292
+			SELECT Hidden
293
+			FROM users_donor_ranks
294
+			WHERE Hidden = '0'
295
+				AND UserID = '$UserID'");
296
+		$HasResults = G::$DB->has_results();
297
+		G::$DB->set_query_id($QueryID);
298
+		return $HasResults;
299
+	}
300
+
301
+	public static function has_donor_forum($UserID) {
302
+		return self::get_rank($UserID) >= DONOR_FORUM_RANK || self::get_special_rank($UserID) >= MAX_SPECIAL_RANK;
303
+	}
304
+
305
+	/**
306
+	 * Put all the common donor info in the same cache key to save some cache calls
307
+	 */
308
+	public static function get_donor_info($UserID) {
309
+		// Our cache class should prevent identical memcached requests
310
+		$DonorInfo = G::$Cache->get_value("donor_info_$UserID");
311
+		if ($DonorInfo === false) {
312
+			$QueryID = G::$DB->get_query_id();
313
+			G::$DB->query("
314
+				SELECT
315
+					Rank,
316
+					SpecialRank,
317
+					TotalRank,
318
+					DonationTime,
319
+					RankExpirationTime + INTERVAL 766 HOUR
320
+				FROM users_donor_ranks
321
+				WHERE UserID = '$UserID'");
322
+				// 2 hours less than 32 days to account for schedule run times
323
+			if (G::$DB->has_results()) {
324
+				list($Rank, $SpecialRank, $TotalRank, $DonationTime, $ExpireTime) = G::$DB->next_record(MYSQLI_NUM, false);
325
+				if ($DonationTime === null) {
326
+					$DonationTime = 0;
327
+				}
328
+				if ($ExpireTime === null) {
329
+					$ExpireTime = 0;
330
+				}
331
+			} else {
332
+				$Rank = $SpecialRank = $TotalRank = $DonationTime = $ExpireTime = 0;
333
+			}
334
+			if (Permissions::is_mod($UserID)) {
335
+				$Rank = MAX_EXTRA_RANK;
336
+				$SpecialRank = MAX_SPECIAL_RANK;
337
+			}
338
+			G::$DB->query("
339
+				SELECT
340
+					IconMouseOverText,
341
+					AvatarMouseOverText,
342
+					CustomIcon,
343
+					CustomIconLink,
344
+					SecondAvatar
345
+				FROM donor_rewards
346
+				WHERE UserID = '$UserID'");
347
+			$Rewards = G::$DB->next_record(MYSQLI_ASSOC);
348
+			G::$DB->set_query_id($QueryID);
349
+
350
+			$DonorInfo = array(
351
+				'Rank' => (int)$Rank,
352
+				'SRank' => (int)$SpecialRank,
353
+				'TotRank' => (int)$TotalRank,
354
+				'Time' => $DonationTime,
355
+				'ExpireTime' => $ExpireTime,
356
+				'Rewards' => $Rewards);
357
+			G::$Cache->cache_value("donor_info_$UserID", $DonorInfo, 0);
358
+		}
359
+		return $DonorInfo;
360
+	}
361
+
362
+	public static function get_rank($UserID) {
363
+		return self::get_donor_info($UserID)['Rank'];
364
+	}
365
+
366
+	public static function get_special_rank($UserID) {
367
+		return self::get_donor_info($UserID)['SRank'];
368
+	}
369
+
370
+	public static function get_total_rank($UserID) {
371
+		return self::get_donor_info($UserID)['TotRank'];
372
+	}
373
+
374
+	public static function get_donation_time($UserID) {
375
+		return self::get_donor_info($UserID)['Time'];
376
+	}
377
+
378
+	public static function get_personal_collages($UserID) {
379
+		$DonorInfo = self::get_donor_info($UserID);
380
+		if ($DonorInfo['SRank'] == MAX_SPECIAL_RANK) {
381
+			$Collages = 5;
382
+		} else {
383
+			$Collages = min($DonorInfo['Rank'], 5); // One extra collage per donor rank up to 5
384
+		}
385
+		return $Collages;
386
+	}
387
+
388
+	public static function get_titles($UserID) {
389
+		$Results = G::$Cache->get_value("donor_title_$UserID");
390
+		if ($Results === false) {
391
+			$QueryID = G::$DB->get_query_id();
392
+			G::$DB->query("
393
+				SELECT Prefix, Suffix, UseComma
394
+				FROM donor_forum_usernames
395
+				WHERE UserID = '$UserID'");
396
+			$Results = G::$DB->next_record();
397
+			G::$DB->set_query_id($QueryID);
398
+			G::$Cache->cache_value("donor_title_$UserID", $Results, 0);
399
+		}
400
+		return $Results;
401
+	}
402
+
403
+
404
+
405
+	public static function get_enabled_rewards($UserID) {
406
+		$Rewards = array();
407
+		$Rank = self::get_rank($UserID);
408
+		$SpecialRank = self::get_special_rank($UserID);
409
+		$HasAll = $SpecialRank == 3;
410
+
411
+		$Rewards = array(
412
+			'HasAvatarMouseOverText' => false,
413
+			'HasCustomDonorIcon' => false,
414
+			'HasDonorForum' => false,
415
+			'HasDonorIconLink' => false,
416
+			'HasDonorIconMouseOverText' => false,
417
+			'HasProfileInfo1' => false,
418
+			'HasProfileInfo2' => false,
419
+			'HasProfileInfo3' => false,
420
+			'HasProfileInfo4' => false,
421
+			'HasSecondAvatar' => false);
422
+
423
+//		if ($Rank >= 1 || $HasAll) {
424
+//
425
+//		}
426
+		if ($Rank >= 2 || $HasAll) {
427
+			$Rewards["HasDonorIconMouseOverText"] = true;
428
+			$Rewards["HasProfileInfo1"] = true;
429
+		}
430
+		if ($Rank >= 3 || $HasAll) {
431
+			$Rewards["HasAvatarMouseOverText"] = true;
432
+			$Rewards["HasProfileInfo2"] = true;
433
+		}
434
+		if ($Rank >= 4 || $HasAll) {
435
+			$Rewards["HasDonorIconLink"] = true;
436
+			$Rewards["HasProfileInfo3"] = true;
437
+		}
438
+		if ($Rank >= MAX_RANK || $HasAll) {
439
+			$Rewards["HasCustomDonorIcon"] = true;
440
+			$Rewards["HasDonorForum"] = true;
441
+			$Rewards["HasProfileInfo4"] = true;
442
+		}
443
+		if ($SpecialRank >= 2) {
444
+			$Rewards["HasSecondAvatar"] = true;
445
+		}
446
+		return $Rewards;
447
+	}
448
+
449
+	public static function get_rewards($UserID) {
450
+		return self::get_donor_info($UserID)['Rewards'];
451
+	}
452
+
453
+	public static function get_profile_rewards($UserID) {
454
+		$Results = G::$Cache->get_value("donor_profile_rewards_$UserID");
455
+		if ($Results === false) {
456
+			$QueryID = G::$DB->get_query_id();
457
+			G::$DB->query("
458
+				SELECT
459
+					ProfileInfo1,
460
+					ProfileInfoTitle1,
461
+					ProfileInfo2,
462
+					ProfileInfoTitle2,
463
+					ProfileInfo3,
464
+					ProfileInfoTitle3,
465
+					ProfileInfo4,
466
+					ProfileInfoTitle4
467
+				FROM donor_rewards
468
+				WHERE UserID = '$UserID'");
469
+			$Results = G::$DB->next_record();
470
+			G::$DB->set_query_id($QueryID);
471
+			G::$Cache->cache_value("donor_profile_rewards_$UserID", $Results, 0);
472
+		}
473
+		return $Results;
474
+	}
475
+
476
+	private static function add_profile_info_reward($Counter, &$Insert, &$Values, &$Update) {
477
+		if (isset($_POST["profile_title_" . $Counter]) && isset($_POST["profile_info_" . $Counter])) {
478
+			$ProfileTitle = db_string($_POST["profile_title_" . $Counter]);
479
+			$ProfileInfo = db_string($_POST["profile_info_" . $Counter]);
480
+			$ProfileInfoTitleSQL = "ProfileInfoTitle" . $Counter;
481
+			$ProfileInfoSQL = "ProfileInfo" . $Counter;
482
+			$Insert[] = "$ProfileInfoTitleSQL";
483
+			$Values[] = "'$ProfileInfoTitle'";
484
+			$Update[] = "$ProfileInfoTitleSQL = '$ProfileTitle'";
485
+			$Insert[] = "$ProfileInfoSQL";
486
+			$Values[] = "'$ProfileInfo'";
487
+			$Update[] = "$ProfileInfoSQL = '$ProfileInfo'";
488
+		}
489
+	}
490
+
491
+
492
+
493
+	public static function update_rewards($UserID) {
494
+		$Rank = self::get_rank($UserID);
495
+		$SpecialRank = self::get_special_rank($UserID);
496
+		$HasAll = $SpecialRank == 3;
497
+		$Counter = 0;
498
+		$Insert = array();
499
+		$Values = array();
500
+		$Update = array();
501
+
502
+		$Insert[] = "UserID";
503
+		$Values[] = "'$UserID'";
504
+		if ($Rank >= 1 || $HasAll) {
505
+
506
+		}
507
+		if ($Rank >= 2 || $HasAll) {
508
+			if (isset($_POST['donor_icon_mouse_over_text'])) {
509
+				$IconMouseOverText = db_string($_POST['donor_icon_mouse_over_text']);
510
+				$Insert[] = "IconMouseOverText";
511
+				$Values[] = "'$IconMouseOverText'";
512
+				$Update[] = "IconMouseOverText = '$IconMouseOverText'";
513
+			}
514
+			$Counter++;
515
+		}
516
+		if ($Rank >= 3 || $HasAll) {
517
+			if (isset($_POST['avatar_mouse_over_text'])) {
518
+				$AvatarMouseOverText = db_string($_POST['avatar_mouse_over_text']);
519
+				$Insert[] = "AvatarMouseOverText";
520
+				$Values[] = "'$AvatarMouseOverText'";
521
+				$Update[] = "AvatarMouseOverText = '$AvatarMouseOverText'";
522
+			}
523
+			$Counter++;
524
+		}
525
+		if ($Rank >= 4 || $HasAll) {
526
+			if (isset($_POST['donor_icon_link'])) {
527
+				$CustomIconLink = db_string($_POST['donor_icon_link']);
528
+				if (!Misc::is_valid_url($CustomIconLink)) {
529
+					$CustomIconLink = '';
530
+				}
531
+				$Insert[] = "CustomIconLink";
532
+				$Values[] = "'$CustomIconLink'";
533
+				$Update[] = "CustomIconLink = '$CustomIconLink'";
534
+			}
535
+			$Counter++;
536
+		}
537
+		if ($Rank >= MAX_RANK || $HasAll) {
538
+			if (isset($_POST['donor_icon_custom_url'])) {
539
+				$CustomIcon = db_string($_POST['donor_icon_custom_url']);
540
+				if (!Misc::is_valid_url($CustomIcon)) {
541
+					$CustomIcon = '';
542
+				}
543
+				$Insert[] = "CustomIcon";
544
+				$Values[] = "'$CustomIcon'";
545
+				$Update[] = "CustomIcon = '$CustomIcon'";
546
+			}
547
+			self::update_titles($UserID, $_POST['donor_title_prefix'], $_POST['donor_title_suffix'], $_POST['donor_title_comma']);
548
+			$Counter++;
549
+		}
550
+		for ($i = 1; $i <= $Counter; $i++) {
551
+			self::add_profile_info_reward($i, $Insert, $Values, $Update);
552
+		}
553
+		if ($SpecialRank >= 2) {
554
+			if (isset($_POST['second_avatar'])) {
555
+				$SecondAvatar = db_string($_POST['second_avatar']);
556
+				if (!Misc::is_valid_url($SecondAvatar)) {
557
+					$SecondAvatar = '';
558
+				}
559
+				$Insert[] = "SecondAvatar";
560
+				$Values[] = "'$SecondAvatar'";
561
+				$Update[] = "SecondAvatar = '$SecondAvatar'";
562
+			}
563
+		}
564
+		$Insert = implode(', ', $Insert);
565
+		$Values = implode(', ', $Values);
566
+		$Update = implode(', ', $Update);
567
+		if ($Counter > 0) {
568
+			$QueryID = G::$DB->get_query_id();
569
+			G::$DB->query("
570
+				INSERT INTO donor_rewards
571
+					($Insert)
572
+				VALUES
573
+					($Values)
574
+				ON DUPLICATE KEY UPDATE
575
+					$Update");
576
+			G::$DB->set_query_id($QueryID);
577
+		}
578
+		G::$Cache->delete_value("donor_profile_rewards_$UserID");
579
+		G::$Cache->delete_value("donor_info_$UserID");
580
+
581
+	}
582
+
583
+	public static function update_titles($UserID, $Prefix, $Suffix, $UseComma) {
584
+		$QueryID = G::$DB->get_query_id();
585
+		$Prefix = trim(db_string($Prefix));
586
+		$Suffix = trim(db_string($Suffix));
587
+		$UseComma = empty($UseComma);
588
+		G::$DB->query("
589
+			INSERT INTO donor_forum_usernames
590
+				(UserID, Prefix, Suffix, UseComma)
591
+			VALUES
592
+				('$UserID', '$Prefix', '$Suffix', '$UseComma')
593
+			ON DUPLICATE KEY UPDATE
594
+				Prefix = '$Prefix',
595
+				Suffix = '$Suffix',
596
+				UseComma = '$UseComma'");
597
+		G::$Cache->delete_value("donor_title_$UserID");
598
+		G::$DB->set_query_id($QueryID);
599
+	}
600
+
601
+
602
+	public static function get_donation_history($UserID) {
603
+		$UserID = (int)$UserID;
604
+		if (empty($UserID)) {
605
+			error(404);
606
+		}
607
+		$QueryID = G::$DB->get_query_id();
608
+		G::$DB->query("
609
+			SELECT Amount, Email, Time, Currency, Reason, Source, AddedBy, Rank, TotalRank
610
+			FROM donations
611
+			WHERE UserID = '$UserID'
612
+			ORDER BY Time DESC");
613
+		$DonationHistory = G::$DB->to_array(false, MYSQLI_ASSOC, false);
614
+		G::$DB->set_query_id($QueryID);
615
+		return $DonationHistory;
616
+	}
617
+
618
+	public static function get_rank_expiration($UserID) {
619
+		$DonorInfo = self::get_donor_info($UserID);
620
+		if ($DonorInfo['SRank'] == MAX_SPECIAL_RANK || $DonorInfo['Rank'] == 1) {
621
+			$Return = 'Never';
622
+		} elseif ($DonorInfo['ExpireTime']) {
623
+			$ExpireTime = strtotime($DonorInfo['ExpireTime']);
624
+			if ($ExpireTime - time() < 60) {
625
+				$Return = 'Soon';
626
+			} else {
627
+				$Expiration = time_diff($ExpireTime); // 32 days
628
+				$Return = "in $Expiration";
629
+			}
630
+		} else {
631
+			$Return = '';
632
+		}
633
+		return $Return;
634
+	}
635
+
636
+	public static function get_leaderboard_position($UserID) {
637
+		$UserID = (int)$UserID;
638
+		$QueryID = G::$DB->get_query_id();
639
+		G::$DB->query("SET @RowNum := 0");
640
+		G::$DB->query("
641
+			SELECT Position
642
+			FROM (
643
+				SELECT d.UserID, @RowNum := @RowNum + 1 AS Position
644
+				FROM users_donor_ranks AS d
645
+				ORDER BY TotalRank DESC
646
+			) l
647
+			WHERE UserID = '$UserID'");
648
+		if (G::$DB->has_results()) {
649
+			list($Position) = G::$DB->next_record();
650
+		} else {
651
+			$Position = 0;
652
+		}
653
+		G::$DB->set_query_id($QueryID);
654
+		return $Position;
655
+	}
656
+
657
+	public static function is_donor($UserID) {
658
+		return self::get_rank($UserID) > 0;
659
+	}
660
+
661
+	public static function currency_exchange($Amount, $Currency) {
662
+		if (!self::is_valid_currency($Currency)) {
663
+			error("$Currency is not valid currency");
664
+		}
665
+		switch ($Currency) {
666
+			case 'USD':
667
+				$Amount = self::usd_to_euro($Amount);
668
+				break;
669
+			case 'BTC':
670
+				$Amount = self::btc_to_euro($Amount);
671
+				break;
672
+			default:
673
+				break;
674
+		}
675
+		return round($Amount, 2);
676
+	}
677
+
678
+	public static function is_valid_currency($Currency) {
679
+		return $Currency == 'EUR' || $Currency == 'BTC' || $Currency == 'USD';
680
+	}
681
+
682
+	public static function btc_to_euro($Amount) {
683
+		$Rate = G::$Cache->get_value('btc_rate');
684
+		if (empty($Rate)) {
685
+			$Rate = self::get_stored_conversion_rate('BTC');
686
+			G::$Cache->cache_value('btc_rate', $Rate, 86400);
687
+		}
688
+		return $Rate * $Amount;
689
+	}
690
+
691
+	public static function usd_to_euro($Amount) {
692
+		$Rate = G::$Cache->get_value('usd_rate');
693
+		if (empty($Rate)) {
694
+			$Rate = self::get_stored_conversion_rate('USD');
695
+			G::$Cache->cache_value('usd_rate', $Rate, 86400);
696
+		}
697
+		return $Rate * $Amount;
698
+	}
699
+
700
+	public static function get_stored_conversion_rate($Currency) {
701
+		$QueryID = G::$DB->get_query_id();
702
+		G::$DB->query("
703
+			SELECT Rate
704
+			FROM currency_conversion_rates
705
+			WHERE Currency = '$Currency'");
706
+		list($Rate) = G::$DB->next_record(MYSQLI_NUM, false);
707
+		G::$DB->set_query_id($QueryID);
708
+		return $Rate;
709
+	}
710
+
711
+	private static function set_stored_conversion_rate($Currency, $Rate) {
712
+		$QueryID = G::$DB->get_query_id();
713
+		G::$DB->query("
714
+			REPLACE INTO currency_conversion_rates
715
+				(Currency, Rate, Time)
716
+			VALUES
717
+				('$Currency', $Rate, NOW())");
718
+		if ($Currency == 'USD') {
719
+			$KeyName = 'usd_rate';
720
+		} elseif ($Currency == 'BTC') {
721
+			$KeyName = 'btc_rate';
722
+		}
723
+		G::$Cache->cache_value($KeyName, $Rate, 86400);
724
+		G::$DB->set_query_id($QueryID);
725
+	}
726
+
727
+	private static function get_new_conversion_rates() {
728
+		if ($BTC = file_get_contents(BTC_API_URL)) {
729
+			$BTC = json_decode($BTC, true);
730
+			if (isset($BTC['24h_avg'])) {
731
+				if ($Rate = round($BTC['24h_avg'], 4)) { // We don't need good precision
732
+					self::set_stored_conversion_rate('BTC', $Rate);
733
+				}
734
+			}
735
+		}
736
+		if ($USD = file_get_contents(USD_API_URL)) {
737
+			// Valid JSON isn't returned so we make it valid.
738
+			$Replace = array(
739
+				'lhs' => '"lhs"',
740
+				'rhs' => '"rhs"',
741
+				'error' => '"error"',
742
+				'icc' => '"icc"'
743
+			);
744
+
745
+			$USD = str_replace(array_keys($Replace), array_values($Replace), $USD);
746
+			$USD = json_decode($USD, true);
747
+			if (isset($USD['rhs'])) {
748
+				// The response is in format "# Euroes", extracts the numbers.
749
+				$Rate = preg_split("/[\s,]+/", $USD['rhs']);
750
+				if ($Rate = round($Rate[0], 4)) { // We don't need good precision
751
+					self::set_stored_conversion_rate('USD', $Rate);
752
+				}
753
+			}
754
+		}
755
+	}
756
+
757
+	public static function get_forum_description() {
758
+		return self::$ForumDescriptions[rand(0, count(self::$ForumDescriptions) - 1)];
759
+	}
760
+
761
+	private static function get_pm_body($Source, $Currency, $DonationAmount, $ReceivedRank, $CurrentRank) {
762
+		if ($Currency != 'BTC') {
763
+			$DonationAmount = number_format($DonationAmount, 2);
764
+		}
765
+		if ($CurrentRank >= MAX_RANK) {
766
+			$CurrentRank = MAX_RANK - 1;
767
+		} elseif ($CurrentRank == 5) {
768
+			$CurrentRank = 4;
769
+		}
770
+		return "Thank you for your generosity and support. It's users like you who make all of this possible. What follows is a brief description of your transaction:
771
+[*][b]You Contributed:[/b] $DonationAmount $Currency
772
+[*][b]You Received:[/b] $ReceivedRank Donor Point".($ReceivedRank == 1 ? '' : 's')."
773
+[*][b]Your Donor Rank:[/b] Donor Rank # $CurrentRank
774
+Once again, thank you for your continued support of the site.
775
+
776
+Sincerely,
777
+
778
+".SITE_NAME.' Staff
779
+
780
+[align=center][If you have any questions or concerns, please [url='.site_url().'staffpm.php]send a Staff PM[/url].]';
781
+	}
782
+
783
+	private static function get_special_rank_one_pm() {
784
+		return 'Congratulations on reaching [url='.site_url().'forums.php?action=viewthread&threadid=178640&postid=4839790#post4839790]Special Rank #1[/url]! You\'ve been awarded [b]one user pick[/b]! This user pick will be featured on the '.SITE_NAME.' front page during an upcoming event. After you submit your pick, there is no guarantee as to how long it will take before your pick is featured. Picks will be featured on a first-submitted, first-served basis. Please abide by the following guidelines when making your selection:
785
+
786
+[*]Pick something that hasn\'t been chosen. You can tell if a pick has been used previously by looking at the collages it\'s in.
787
+[*]Complete the enclosed form carefully and completely.
788
+[*]Send a [url='.site_url().'staffpm.php]Staff PM[/url] with the completed form. Title this PM "Special Rank User Pick".
789
+[important][align=center]**The following form must be used. Do not edit the BBCode. Reply to this message with the completed form.**[/align][/important]
790
+
791
+[quote][align=center][size=10]SUBMISSION FORM[/size]
792
+[size=4]COPY & PASTE THE BBCODE INTO YOUR REPLY. FILL FORM WITH YOUR INFO. SUBMIT.[/size][/align]
793
+
794
+
795
+[code][align=center][size=10][u]FRONT PAGE[/u][/size][/align]
796
+
797
+
798
+[b][size=5][user]<NAME>[/user]\'s Special Donor Pick[/size][/b]
799
+
800
+[b]Artist Name - Album Title[/b]
801
+
802
+[b]Genre:[/b] Alternative, Rock, Grunge, ...
803
+
804
+[b]Torrents:[/b] Torrent Group Link Here
805
+
806
+[b]Review:[/b] [quote]Put your front page review here. The review should be a maximum of two small(ish) paragraphs or one medium-sized paragraph. Do not include a huge review for the front page portion of your post, as it will be truncated per the wishes of the Staff Team.[/quote]
807
+
808
+
809
+
810
+
811
+
812
+[align=center][size=10][u]FORUM POST[/u][/size][/align]
813
+
814
+
815
+[b][size=5][user]<NAME>[/user]\'s Special Donor Pick[/size][/b]
816
+
817
+[img]Album Cover URL[/img]
818
+
819
+[b]Artist Name - Album Title[/b]
820
+
821
+[b]Genre:[/b] Alternative, Rock, Grunge, ...
822
+
823
+[b]Torrents:[/b] Torrent Group Link Here
824
+
825
+[b]Release Info:[/b]
826
+[*][u]Release Date[/u]: Release Date Here
827
+[*][u]Tracks[/u]: Track Count Here
828
+[*][u]Length[/u]: Release Length Here
829
+[*][u]Label[/u]: Release Label Here
830
+
831
+[b]Credits:[/b]
832
+[*][b]Member Name 1:[/b] Role / Instruments Played
833
+[*][b]Member Name 2:[/b] Role / Instruments Played
834
+[*][b]Member Name 3:[/b] Role / Instruments Played
835
+
836
+[b]Track Listing:[/b]
837
+[#]Remove or add to this list...
838
+[#]...as is necessary.
839
+[#]Track 03
840
+[#]Track 04
841
+[#]Track 05
842
+[#]Track 06
843
+[#]Track 07
844
+[#]Track 08
845
+[#]Track 09
846
+[#]Track 10
847
+
848
+[b]Web site:[/b] Artist Web Site / Info Page / Fan Site Link Here
849
+
850
+[b]Review:[/b] [quote]Put your forum post review here. This review can be any length, but please be reasonable. You can write it yourself or source it from the Internet. Please try to include a source if you don\'t write your own review. If your review is too long, it will be included but put into "hide" tags by the person formatting the thread.[/quote][/code][/quote]
851
+[hide=Completed Submission Form (Example)][align=center][size=10][u]FRONT PAGE[/u][/size][/align]
852
+
853
+
854
+[b][size=5][user]DixieFlatline[/user]\'s Special Donor Pick[/size][/b]
855
+
856
+[b]The Door and The Window - Detailed Twang[/b]
857
+
858
+[b]Genre:[/b] Post-Punk, Rock, Experimental
859
+
860
+[b]Torrents:[/b] https://what.cd/torrents.php?id=649629
861
+
862
+[b]Review:[/b] [quote=Mutant Sounds]Stumblebum idiot savant songform fragmentation that vacillates between moments of delirious jerry-rigged inspiration and passages that border on the insufferably sophomoric, brought to you by this batch of DIY rabble rousers whose stance and attitude (complete with DIY manifestos on the back cover) aligned them strongly at the time with the likes of The Desperate Bicycles. Loosely yoked to a whole network of willfully rinky dink art damage, Nag and Bendle (2/3 of The Door And The Window) also participated in The 49 Americans, a group with a similar propensity for inspired faux naif art brut whimsy and Mark Perry (the other 1/3) was of course the leader of Alternative TV, whose NWW list included group The Good Missionaries also traffic in much the same sort of abstruse fuckery.[/quote]
863
+
864
+
865
+
866
+
867
+
868
+[align=center][size=10][u]FORUM POST[/u][/size][/align]
869
+
870
+
871
+[b][size=5][user]DixieFlatline[/user]\'s Special Donor Pick[/size][/b]
872
+
873
+[img]https://i.imgur.com/wXdQd.jpg[/img]
874
+
875
+[b]The Door and The Window - Detailed Twang[/b]
876
+
877
+[b]Genre:[/b] Post-Punk, Rock, Experimental
878
+
879
+[b]Torrents:[/b] https://what.cd/torrents.php?id=649629
880
+
881
+[b]Release Info:[/b]
882
+[*][u]Release Date[/u]: 1980 (Reissue: 2003)
883
+[*][u]Tracks[/u]: 12 (Reissue: 23)
884
+[*][u]Length[/u]: 1:09:11
885
+[*][u]Label[/u]: Overground Records
886
+
887
+[b]Credits:[/b]
888
+[*][b]Bendle:[/b] Guitar, Percussion, Vocals
889
+[*][b]Nag:[/b] Percussion, Synthesizer, Vocals
890
+[*][b]Mark Perry:[/b] Drums, Saxophone, Vocals (tracks: 1 to 12, 22, 23)
891
+
892
+[b]Track Listing:[/b]
893
+[#]Dads (4:41)
894
+[#]Habits (2:14)
895
+[#]We Do Scare Each Other (2:23)
896
+[#]Order And Obey (3:20)
897
+[#]He Feels Like A Doris (4:47)
898
+[#]Part-Time Punks (3:50)
899
+[#]In The Car (0:43)
900
+[#]Subculture Fashion Slaves (3:51)
901
+[#]Sticks And Stones (3:56)
902
+[#]Positive (4:24)
903
+[#]Why Must You Build Walls Around Us? (2:17)
904
+[#]Detailed Twang (1:50)
905
+[#]Subculture Fashion Slaves (Early Version) (3:26)
906
+[#]Nostradomus (1:38)
907
+[#]Don\'t Kill Colin (3:33)
908
+[#]Wurst Band (1:38)
909
+[#]Dig (2:10)
910
+[#]Production Line (4:05)
911
+[#]He Feels Like A Doris (Early Version) (3:25)
912
+[#]I Like Sound (1:25)
913
+[#]Innocent (1:38)
914
+[#]The Number One Entertainer (3:38)
915
+[#]C.C.H. (4:19)
916
+
917
+[b]Web site:[/b] https://www.wikipedia.org
918
+
919
+[b]Review:[/b] [quote=Mutant Sounds]Stumblebum idiot savant songform fragmentation that vacillates between moments of delirious jerry-rigged inspiration and passages that border on the insufferably sophomoric, brought to you by this batch of DIY rabble rousers whose stance and attitude (complete with DIY manifestos on the back cover) aligned them strongly at the time with the likes of The Desperate Bicycles. Loosely yoked to a whole network of willfully rinky dink art damage, Nag and Bendle (2/3 of The Door And The Window) also participated in The 49 Americans, a group with a similar propensity for inspired faux naif art brut whimsy and Mark Perry (the other 1/3) was of course the leader of Alternative TV, whose NWW list included group The Good Missionaries also traffic in much the same sort of abstruse fuckery.[/quote][/hide]
920
+
921
+At this time, we\'d like to thank you for your continued support of the site. The fact that you\'ve reached this milestone is testament to your belief in '.SITE_NAME.' as a project. It\'s dedicated users like you that keep us alive. We look forward to featuring your pick in an upcoming announcement.
922
+
923
+Sincerely,
924
+
925
+'.SITE_NAME.' Staff';
926
+	}
927
+
928
+	private static function get_special_rank_two_pm() {
929
+		return 'Congratulations on reaching [url='.site_url().'forums.php?action=viewthread&threadid=178640&postid=4839790#post4839790]Special Rank #2[/url]! You\'ve been awarded [b]double avatar functionality[/b]! To set a second avatar, please enter a URL leading to a valid image in the new field which has been unlocked in your [b]Personal Settings[/b]. Any avatar you choose must abide by normal avatar rules. When running your cursor over your avatar, it will flip to the alternate choice you\'ve established. Other users will also be able to view both of your avatars using this method.
930
+
931
+At this time, we\'d like to thank you for your continued support of the site. The fact that you\'ve reached this milestone is testament to your belief in '.SITE_NAME.' as a project. It\'s dedicated users like you that keep us alive. Have fun with the new toy.
932
+
933
+Sincerely,
934
+
935
+'.SITE_NAME.' Staff';
936
+	}
937
+
938
+	private static function get_special_rank_three_pm() {
939
+		return 'Congratulations on reaching [url='.site_url().'forums.php?action=viewthread&threadid=178640&postid=4839790#post4839790]Special Rank #3[/url]! You\'ve been awarded [b]Diamond Rank[/b]! Diamond Rank grants you the benefits associated with every Donor Rank up to and including Gold ([url='.site_url().'forums.php?action=viewthread&threadid=178640&postid=4839789#post4839789]Donor Rank #5[/url]). But unlike Donor Rank #5 - because Diamond Rank is a Special Rank - it will never expire.
940
+
941
+At this time, we\'d like to thank you for your continued support of the site. The fact that you\'ve reached this milestone is testament to your belief in '.SITE_NAME.' as a project. It\'s dedicated users like you that keep us alive. Consider yourself one of our top supporters!
942
+
943
+Sincerely,
944
+
945
+'.SITE_NAME.' Staff';
946
+	}
947
+
948
+}

+ 186
- 0
classes/donationsbitcoin.class.php View File

1
+<?
2
+class DonationsBitcoin {
3
+	/**
4
+	 * Ask bitcoind for a list of all addresses that have received bitcoins
5
+	 *
6
+	 * @return array (BitcoinAddress => Amount, ...)
7
+	 */
8
+	public static function get_received() {
9
+		if (defined('BITCOIN_RPC_URL')) {
10
+			$Donations = BitcoinRpc::listreceivedbyaddress();
11
+		}
12
+		if (empty($Donations)) {
13
+			return array();
14
+		}
15
+		$BTCUsers = array();
16
+		foreach ($Donations as $Account) {
17
+			$BTCUsers[$Account->address] = $Account->amount;
18
+		}
19
+		return $BTCUsers;
20
+	}
21
+
22
+	/**
23
+	 * Ask bitcoind for the current account balance
24
+	 *
25
+	 * @return float balance
26
+	 */
27
+	public static function get_balance() {
28
+		if (defined('BITCOIN_RPC_URL')) {
29
+			return BitcoinRpc::getbalance();
30
+		}
31
+	}
32
+
33
+	/**
34
+	 * Get a user's existing bitcoin address or generate a new one
35
+	 *
36
+	 * @param int $UserID
37
+	 * @param bool $GenAddress whether to create a new address if it doesn't exist
38
+	 * @return false if no address exists and $GenAddress is false
39
+	 *         string bitcoin address otherwise
40
+	 */
41
+	public static function get_address($UserID, $GenAddress = false) {
42
+		$UserID = (int)$UserID;
43
+		$QueryID = G::$DB->get_query_id();
44
+		G::$DB->query("
45
+			SELECT BitcoinAddress
46
+			FROM users_info
47
+			WHERE UserID = '$UserID'");
48
+		list($Addr) = G::$DB->next_record();
49
+		G::$DB->set_query_id($QueryID);
50
+
51
+		if (!empty($Addr)) {
52
+			return $Addr;
53
+		} elseif ($GenAddress) {
54
+			if (defined('BITCOIN_RPC_URL')) {
55
+				$NewAddr = BitcoinRpc::getnewaddress();
56
+			}
57
+			if (empty($NewAddr)) {
58
+				error(0);
59
+			}
60
+			$QueryID = G::$DB->get_query_id();
61
+			G::$DB->query("
62
+				UPDATE users_info
63
+				SET BitcoinAddress = '".db_string($NewAddr)."'
64
+				WHERE UserID = '$UserID'
65
+					AND BitcoinAddress IS NULL");
66
+			G::$DB->set_query_id($QueryID);
67
+			return $NewAddr;
68
+		} else {
69
+			return false;
70
+		}
71
+	}
72
+
73
+	/**
74
+	 * Ask bitcoind for the total amount of bitcoins received
75
+	 *
76
+	 * @return float amount
77
+	 */
78
+	public static function get_total_received() {
79
+		if (defined('BITCOIN_RPC_URL')) {
80
+			$Accounts = BitcoinRpc::listreceivedbyaccount();
81
+		}
82
+		if (empty($Accounts)) {
83
+			return 0.0;
84
+		}
85
+		foreach ($Accounts as $Account) {
86
+			if ($Account->account == '') {
87
+				return $Account->amount;
88
+			}
89
+		}
90
+		return 0.0;
91
+	}
92
+
93
+	/**
94
+	 * Translate bitcoin addresses to user IDs
95
+	 *
96
+	 * @param array $Addresses list of bitcoin addresses
97
+	 * @return array (BitcoinAddress => UserID, ...)
98
+	 */
99
+	public static function get_userids($Addresses) {
100
+		if (!is_array($Addresses) || empty($Addresses)) {
101
+			return false;
102
+		}
103
+		$QueryID = G::$DB->get_query_id();
104
+		G::$DB->query("
105
+			SELECT BitcoinAddress, UserID
106
+			FROM users_info
107
+			WHERE BitcoinAddress IN ('" . implode("', '", $Addresses) . "')");
108
+		if (G::$DB->has_results()) {
109
+			$UserIDs = G::$DB->to_pair(0, 1);
110
+		} else {
111
+			$UserIDs = array();
112
+		}
113
+		G::$DB->set_query_id($QueryID);
114
+		return $UserIDs;
115
+	}
116
+
117
+	/**
118
+	 * Find and process new donations since the last time this function was called.
119
+	 */
120
+	public static function find_new_donations() {
121
+		global $Debug;
122
+		if (($OldAmount = G::$Cache->get_value('btc_total_received')) === false) {
123
+			$QueryID = G::$DB->get_query_id();
124
+			G::$DB->query("
125
+				SELECT IFNULL(SUM(Amount), 0)
126
+				FROM donations_bitcoin");
127
+			list($OldAmount) = G::$DB->next_record(MYSQLI_NUM, false);
128
+			G::$DB->set_query_id($QueryID);
129
+		}
130
+		$NewAmount = self::get_total_received();
131
+		if ($NewAmount < $OldAmount) {
132
+			// This shouldn't happen. Perhaps bitcoind was restarted recently
133
+			// or the block index was removed. Either way, try again later
134
+			send_irc('PRIVMSG ' . LAB_CHAN . " :Bad bitcoin donation data (is $NewAmount, was $OldAmount). If this persists, something is probably wrong");
135
+			return false;
136
+		}
137
+		if ($NewAmount > $OldAmount) {
138
+			// I really wish we didn't have to do it like this
139
+			$QueryID = G::$DB->get_query_id();
140
+			G::$DB->query("
141
+				SELECT BitcoinAddress, SUM(Amount)
142
+				FROM donations_bitcoin
143
+				GROUP BY BitcoinAddress");
144
+			$OldDonations = G::$DB->to_pair(0, 1, false);
145
+			G::$DB->set_query_id($QueryID);
146
+			$NewDonations = self::get_received();
147
+			foreach ($NewDonations as $Address => &$Amount) {
148
+				if (isset($OldDonations[$Address])) {
149
+					if ($Amount == $OldDonations[$Address]) { // Direct comparison should be fine as everything comes from bitcoind
150
+						unset($NewDonations[$Address]);
151
+						continue;
152
+					}
153
+					$Debug->log_var(array('old' => $OldDonations[$Address], 'new' => $Amount), "New donations from $Address");
154
+					// PHP doesn't do fixed-point math, and json_decode has already botched the precision
155
+					// so let's just round this off to satoshis and pray that we're on a 64 bit system
156
+					$Amount = round($Amount - $OldDonations[$Address], 8);
157
+				}
158
+				$NewDonations[$Address] = $Amount;
159
+			}
160
+			$Debug->log_var($NewDonations, '$NewDonations');
161
+			foreach (self::get_userids(array_keys($NewDonations)) as $Address => $UserID) {
162
+				Donations::regular_donate($UserID, $NewDonations[$Address], 'Bitcoin Parser', '', 'BTC');
163
+				self::store_donation($Address, $NewDonations[$Address]);
164
+			}
165
+			G::$Cache->cache_value('btc_total_received', $NewAmount, 0);
166
+		}
167
+	}
168
+
169
+	/**
170
+	 * Record a donation in the database
171
+	 *
172
+	 * @param string $Address bitcoin address
173
+	 * @param double $Amount amount of bitcoins transferred
174
+	 */
175
+	public static function store_donation($Address, $Amount) {
176
+		if (!is_numeric($Amount) || $Amount <= 0) {
177
+			// Panic!
178
+			return false;
179
+		}
180
+		G::$DB->query("
181
+			INSERT INTO donations_bitcoin
182
+				(BitcoinAddress, Amount)
183
+			VALUES
184
+				('$Address', $Amount)");
185
+	}
186
+}

+ 214
- 0
classes/donationsview.class.php View File

1
+<?
2
+
3
+class DonationsView {
4
+	public static function render_mod_donations($UserID) {
5
+?>
6
+		<table class="layout box" id="donation_box">
7
+			<tr class="colhead">
8
+				<td colspan="2">
9
+					Donor System (add points)
10
+				</td>
11
+			</tr>
12
+			<tr>
13
+				<td class="label">Value:</td>
14
+				<td>
15
+					<input type="text" name="donation_value" onkeypress="return isNumberKey(event);" />
16
+					<select name="donation_currency">
17
+						<option value="EUR">EUR</option>
18
+						<option value="USD">USD</option>
19
+						<option value="BTC">BTC</option>
20
+					</select>
21
+				</td>
22
+			</tr>
23
+			<tr>
24
+				<td class="label">Reason:</td>
25
+				<td><input type="text" class="wide_input_text" name="donation_reason" /></td>
26
+			</tr>
27
+			<tr>
28
+				<td align="right" colspan="2">
29
+					<input type="submit" name="donor_points_submit" value="Add donor points" />
30
+				</td>
31
+			</tr>
32
+		</table>
33
+
34
+		<table class="layout box" id="donor_points_box">
35
+			<tr class="colhead">
36
+				<td colspan="3" class="tooltip" title='Use this tool only when manually correcting values. If crediting donations normally, use the "Donor System (add points)" tool'>
37
+					Donor System (modify values)
38
+				</td>
39
+			</tr>
40
+			<tr>
41
+				<td class="label tooltip" title="Active points determine a user's Donor Rank and do expire.">Active points:</td>
42
+				<td><input type="text" name="donor_rank" onkeypress="return isNumberKey(event);" value="<?=Donations::get_rank($UserID)?>" /></td>
43
+			</tr>
44
+			<tr>
45
+				<td class="label tooltip" title="Total points represent a user's overall total and never expire. Total points determines a user's Special Rank and Donor Leaderboard placement.">Total points:</td>
46
+				<td><input type="text" name="total_donor_rank" onkeypress="return isNumberKey(event);" value="<?=Donations::get_total_rank($UserID)?>" /></td>
47
+			</tr>
48
+			<tr>
49
+				<td class="label">Reason:</td>
50
+				<td><input type="text" class="wide_input_text" name="reason" /></td>
51
+			</tr>
52
+			<tr>
53
+				<td align="right" colspan="2">
54
+					<input type="submit" name="donor_values_submit" value="Change point values" />
55
+				</td>
56
+			</tr>
57
+		</table>
58
+<?
59
+	}
60
+
61
+	public static function render_donor_stats($UserID) {
62
+		$OwnProfile = G::$LoggedUser['ID'] == $UserID;
63
+		if (check_perms("users_mod") || $OwnProfile || Donations::is_visible($UserID)) {
64
+?>
65
+			<div class="box box_info box_userinfo_donor_stats">
66
+				<div class="head colhead_dark">Donor Statistics</div>
67
+				<ul class="stats nobullet">
68
+<?
69
+			if (Donations::is_donor($UserID)) {
70
+				if (check_perms('users_mod') || $OwnProfile) {
71
+?>
72
+					<li>
73
+						Total donor points: <?=Donations::get_total_rank($UserID)?>
74
+					</li>
75
+<?				} ?>
76
+					<li>
77
+						Current donor rank: <?=self::render_rank(Donations::get_rank($UserID), Donations::get_special_rank($UserID), true)?>
78
+					</li>
79
+					<li>
80
+						Leaderboard position: <?=Donations::get_leaderboard_position($UserID)?>
81
+					</li>
82
+					<li>
83
+						Last donated: <?=time_diff(Donations::get_donation_time($UserID))?>
84
+					</li>
85
+					<li>
86
+						Rank expires: <?=(Donations::get_rank_expiration($UserID))?>
87
+					</li>
88
+<?			} else { ?>
89
+					<li>
90
+						This user hasn't donated.
91
+					</li>
92
+<?			} ?>
93
+				</ul>
94
+			</div>
95
+<?
96
+		}
97
+	}
98
+
99
+	public static function render_profile_rewards($EnabledRewards, $ProfileRewards) {
100
+		for ($i = 1; $i <= 4; $i++) {
101
+			if ($EnabledRewards['HasProfileInfo' . $i] && $ProfileRewards['ProfileInfo' . $i]) {
102
+?>
103
+			<div class="box">
104
+				<div class="head">
105
+					<span><?=!empty($ProfileRewards['ProfileInfoTitle' . $i]) ? display_str($ProfileRewards['ProfileInfoTitle' . $i]) : "Extra Profile " . ($i + 1)?></span>
106
+					<span style="float: right;"><a href="#" onclick="$('#profilediv_<?=$i?>').gtoggle(); this.innerHTML = (this.innerHTML == 'Hide' ? 'Show' : 'Hide'); return false;" class="brackets">Hide</a></span>
107
+				</div>
108
+				<div class="pad profileinfo" id="profilediv_<?=$i?>">
109
+<?					echo Text::full_format($ProfileRewards['ProfileInfo' . $i]); ?>
110
+				</div>
111
+			</div>
112
+<?
113
+			}
114
+		}
115
+	}
116
+
117
+	public static function render_donation_history($DonationHistory) {
118
+		if (empty($DonationHistory)) {
119
+			return;
120
+		}
121
+?>
122
+		<div class="box box2" id="donation_history_box">
123
+			<div class="head">
124
+				Donation History <a href="#" onclick="$('#donation_history').gtoggle(); return false;" class="brackets" style="float: right;">Toggle</a>
125
+			</div>
126
+			<div class="hidden" id="donation_history">
127
+				<table cellpadding="6" cellspacing="1" border="0" class="border" width="100%">
128
+					<tbody>
129
+					<tr class="colhead_dark">
130
+						<td>
131
+							<strong>Source</strong>
132
+						</td>
133
+						<td>
134
+							<strong>Date</strong>
135
+						</td>
136
+						<td>
137
+							<strong>Amount (EUR)</strong>
138
+						</td>
139
+						<td>
140
+							<strong>Added Points</strong>
141
+						</td>
142
+						<td>
143
+							<strong>Total Points</strong>
144
+						</td>
145
+						<td>
146
+							<strong>Email</strong>
147
+						</td>
148
+						<td style="width: 30%;">
149
+							<strong>Reason</strong>
150
+						</td>
151
+					</tr>
152
+<?		foreach ($DonationHistory as $Donation) { ?>
153
+					<tr class="row">
154
+						<td>
155
+							<?=display_str($Donation['Source'])?> (<?=Users::format_username($Donation['AddedBy'])?>)
156
+						</td>
157
+						<td>
158
+							<?=$Donation['Time']?>
159
+						</td>
160
+						<td>
161
+							<?=$Donation['Amount']?>
162
+						</td>
163
+						<td>
164
+							<?=$Donation['Rank']?>
165
+						</td>
166
+						<td>
167
+							<?=$Donation['TotalRank']?>
168
+						</td>
169
+						<td>
170
+							<?=display_str($Donation['Email'])?>
171
+						</td>
172
+						<td>
173
+							<?=display_str($Donation['Reason'])?>
174
+						</td>
175
+					</tr>
176
+<?
177
+		}
178
+?>
179
+					</tbody>
180
+				</table>
181
+			</div>
182
+		</div>
183
+<?
184
+	}
185
+
186
+	public static function render_rank($Rank, $SpecialRank, $ShowOverflow = false) {
187
+		if ($SpecialRank == 3) {
188
+			$Display = '∞ [Diamond]';
189
+		} else {
190
+			$CurrentRank = $Rank >= MAX_RANK ? MAX_RANK : $Rank;
191
+			$Overflow = $Rank - $CurrentRank;
192
+			$Display = $CurrentRank;
193
+			if ($Display == 5 || $Display == 6) {
194
+				$Display--;
195
+			}
196
+			if ($ShowOverflow && $Overflow) {
197
+				$Display .= " (+$Overflow)";
198
+			}
199
+			if ($Rank >= 6) {
200
+				$Display .= ' [Gold]';
201
+			} elseif ($Rank >= 4) {
202
+				$Display .= ' [Silver]';
203
+			} elseif ($Rank >= 3) {
204
+				$Display .= ' [Bronze]';
205
+			} elseif ($Rank >= 2) {
206
+				$Display .= ' [Copper]';
207
+			} elseif ($Rank >= 1) {
208
+				$Display .= ' [Red]';
209
+			}
210
+		}
211
+		echo $Display;
212
+	}
213
+
214
+}

+ 34
- 0
classes/encrypt.class.php View File

1
+<?
2
+/*************************************************************************|
3
+|--------------- Encryption class ----------------------------------------|
4
+|*************************************************************************|
5
+
6
+This class handles encryption and decryption, that's all folks.
7
+
8
+|*************************************************************************/
9
+
10
+if (!extension_loaded('mcrypt')) {
11
+	die('Mcrypt Extension not loaded.');
12
+}
13
+
14
+class CRYPT {
15
+	public function encrypt($Str, $Key = ENCKEY) {
16
+		srand();
17
+		$Str = str_pad($Str, 32 - strlen($Str));
18
+		$IVSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
19
+		$IV = mcrypt_create_iv($IVSize, MCRYPT_RAND);
20
+		$CryptStr = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $Key, $Str, MCRYPT_MODE_CBC, $IV);
21
+		return base64_encode($IV.$CryptStr);
22
+	}
23
+
24
+	public function decrypt($CryptStr, $Key = ENCKEY) {
25
+		if ($CryptStr != '') {
26
+			$IV = substr(base64_decode($CryptStr), 0, 16);
27
+			$CryptStr = substr(base64_decode($CryptStr), 16);
28
+			return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $Key, $CryptStr, MCRYPT_MODE_CBC, $IV));
29
+		} else {
30
+			return '';
31
+		}
32
+	}
33
+} // class ENCRYPT()
34
+?>

+ 73
- 0
classes/feed.class.php View File

1
+<?
2
+class FEED {
3
+	var $UseSSL = true; // If we're using SSL for blog and news links
4
+
5
+	function open_feed() {
6
+		header("Content-type: application/xml; charset=UTF-8");
7
+		echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n","<rss xmlns:dc=\"http://purl.org/dc/elements/1.1/\" version=\"2.0\">\n\t<channel>\n";
8
+	}
9
+
10
+	function close_feed() {
11
+		echo "\t</channel>\n</rss>";
12
+	}
13
+
14
+	function channel($Title, $Description, $Section = '') {
15
+		$Site = $this->UseSSL ? site_url() : site_url(false);
16
+		echo "\t\t<title>$Title :: ". SITE_NAME. "</title>\n";
17
+		echo "\t\t<link>$Site$Section</link>\n";
18
+		echo "\t\t<description>$Description</description>\n";
19
+		echo "\t\t<language>en-us</language>\n";
20
+		echo "\t\t<lastBuildDate>". date('r'). "</lastBuildDate>\n";
21
+		echo "\t\t<docs>http://blogs.law.harvard.edu/tech/rss</docs>\n";
22
+		echo "\t\t<generator>Gazelle Feed Class</generator>\n\n";
23
+	}
24
+
25
+	function item($Title, $Description, $Page, $Creator, $Comments = '', $Category = '', $Date = '') { //Escape with CDATA, otherwise the feed breaks.
26
+		if ($Date == '') {
27
+			$Date = date('r');
28
+		} else {
29
+			$Date = date('r', strtotime($Date));
30
+		}
31
+		$Site = $this->UseSSL ? site_url() : site_url(false);
32
+		$Item = "\t\t<item>\n";
33
+		$Item .= "\t\t\t<title><![CDATA[$Title]]></title>\n";
34
+		$Item .= "\t\t\t<description><![CDATA[$Description]]></description>\n";
35
+		$Item .= "\t\t\t<pubDate>$Date</pubDate>\n";
36
+		$Item .= "\t\t\t<link>$Site$Page</link>\n";
37
+		$Item .= "\t\t\t<guid>$Site$Page</guid>\n";
38
+		if ($Comments != '') {
39
+			$Item .= "\t\t\t<comments>$Site$Comments</comments>\n";
40
+		}
41
+		if ($Category != '') {
42
+			$Item .= "\t\t\t<category><![CDATA[$Category]]></category>\n";
43
+		}
44
+		$Item .= "\t\t\t<dc:creator>$Creator</dc:creator>\n\t\t</item>\n";
45
+		return $Item;
46
+	}
47
+
48
+	function retrieve($CacheKey, $AuthKey, $PassKey) {
49
+		global $Cache;
50
+		$Entries = $Cache->get_value($CacheKey);
51
+		if (!$Entries) {
52
+			$Entries = array();
53
+		} else {
54
+			foreach ($Entries as $Item) {
55
+				echo str_replace(array('[[PASSKEY]]', '[[AUTHKEY]]'), array(display_str($PassKey), display_str($AuthKey)), $Item);
56
+			}
57
+		}
58
+	}
59
+
60
+	function populate($CacheKey, $Item) {
61
+		global $Cache;
62
+		$Entries = $Cache->get_value($CacheKey, true);
63
+		if (!$Entries) {
64
+			$Entries = array();
65
+		} else {
66
+			if (count($Entries) >= 50) {
67
+				array_pop($Entries);
68
+			}
69
+		}
70
+		array_unshift($Entries, $Item);
71
+		$Cache->cache_value($CacheKey, $Entries, 0); //inf cache
72
+	}
73
+}

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

1
+<?php
2
+$ComicsExtensions = array_fill_keys(array('cbr', 'cbz', 'gif', 'jpeg', 'jpg', 'pdf', 'png'), true);
3
+$MusicExtensions = array_fill_keys(
4
+	array(
5
+		'ac3', 'accurip', 'azw3', 'chm', 'cue', 'djv', 'djvu', 'doc', 'dts', 'epub', 'ffp',
6
+		'flac', 'gif', 'htm', 'html', 'jpeg', 'jpg', 'lit', 'log', 'm3u', 'm3u8', 'm4a', 'm4b',
7
+		'md5', 'mobi', 'mp3', 'mp4', 'nfo', 'pdf', 'pls', 'png', 'rtf', 'sfv', 'txt'),
8
+	true);
9
+$Keywords = array(
10
+	'ahashare.com', 'demonoid.com', 'demonoid.me', 'djtunes.com', 'h33t', 'housexclusive.net',
11
+	'limetorrents.com', 'mixesdb.com', 'mixfiend.blogstop', 'mixtapetorrent.blogspot',
12
+	'plixid.com', 'reggaeme.com' , 'scc.nfo', 'thepiratebay.org', 'torrentday');
13
+
14
+function check_file($Type, $Name) {
15
+	check_name($Name);
16
+	check_extensions($Type, $Name);
17
+}
18
+
19
+function check_name($Name) {
20
+	global $Keywords;
21
+	$NameLC = strtolower($Name);
22
+	foreach ($Keywords as &$Value) {
23
+		if (strpos($NameLC, $Value) !== false) {
24
+			forbidden_error($Name);
25
+		}
26
+	}
27
+	if (preg_match('/INCOMPLETE~\*/i', $Name)) {
28
+		forbidden_error($Name);
29
+	}
30
+
31
+	/*
32
+	 * These characters are invalid in NTFS on Windows systems:
33
+	 *		: ? / < > \ * | "
34
+	 *
35
+	 * TODO: Add "/" to the blacklist. Adding "/" to the blacklist causes problems with nested dirs, apparently.
36
+	 *
37
+	 * Only the following characters need to be escaped (see the link below):
38
+	 *		\ - ^ ]
39
+	 *
40
+	 * http://www.php.net/manual/en/regexp.reference.character-classes.php
41
+	 */
42
+	$AllBlockedChars = ' : ? < > \ * | " ';
43
+	if (preg_match('/[\\:?<>*|"]/', $Name, $Matches)) {
44
+		character_error($Matches[0], $AllBlockedChars);
45
+	}
46
+}
47
+
48
+function check_extensions($Type, $Name) {
49
+	global $MusicExtensions, $ComicsExtensions;
50
+	if ($Type == 'Music' || $Type == 'Audiobooks' || $Type == 'Comedy' || $Type == 'E-Books') {
51
+		if (!isset($MusicExtensions[get_file_extension($Name)])) {
52
+			invalid_error($Name);
53
+		}
54
+	} elseif ($Type == 'Comics') {
55
+		if (!isset($ComicsExtensions[get_file_extension($Name)])) {
56
+			invalid_error($Name);
57
+		}
58
+	}
59
+}
60
+
61
+function get_file_extension($FileName) {
62
+	return strtolower(substr(strrchr($FileName, '.'), 1));
63
+}
64
+
65
+function invalid_error($Name) {
66
+	global $Err;
67
+	$Err = 'The torrent contained one or more invalid files (' . display_str($Name) . ')';
68
+}
69
+
70
+function forbidden_error($Name) {
71
+	global $Err;
72
+	$Err = 'The torrent contained one or more forbidden files (' . display_str($Name) . ')';
73
+}
74
+
75
+function character_error($Character, $AllBlockedChars) {
76
+	global $Err;
77
+	$Err = "One or more of the files or folders in the torrent has a name that contains the forbidden character '$Character'. Please rename the files as necessary and recreate the torrent.<br /><br />\nNote: The complete list of characters that are disallowed are shown below:<br />\n\t\t$AllBlockedChars";
78
+}

BIN
classes/fonts/ARIBLK.TTF View File


BIN
classes/fonts/COMIC.TTF View File


BIN
classes/fonts/COMICBD.TTF View File


BIN
classes/fonts/GEORGIA.TTF View File


BIN
classes/fonts/GEORGIAB.TTF View File


BIN
classes/fonts/GEORGIAI.TTF View File


BIN
classes/fonts/GEORGIAZ.TTF View File


BIN
classes/fonts/IMPACT.TTF View File


BIN
classes/fonts/MISTRAL.TTF View File


+ 107
- 0
classes/fonts/README.TXT View File

1
+This document contains the End User Licensing Agreement, and instructions concerning font installation on the various Windows platforms.
2
+
3
+
4
+Microsoft TrueType Fonts
5
+END-USER LICENSE AGREEMENT FOR MICROSOFT SOFTWARE
6
+---------------------------------------------------
7
+
8
+IMPORTANT - READ CAREFULLY: This Microsoft End-User License Agreement ("EULA") is a legal agreement between you (either an individual or a single entity) and Microsoft Corporation for the Microsoft software accompanying this EULA, which includes computer software and may include associated media, printed materials, and "on-line" or electronic documentation ("SOFTWARE PRODUCT" or "SOFTWARE"). By exercising your rights to make and use copies of the SOFTWARE PRODUCT, you agree to be bound by the terms of this EULA. If you do not agree to the terms of this EULA, you may not use the SOFTWARE PRODUCT.
9
+SOFTWARE PRODUCT LICENSE
10
+The SOFTWARE PRODUCT is protected by copyright laws and international copyright treaties, as well as other intellectual property laws and treaties. The SOFTWARE PRODUCT is licensed, not sold.
11
+1. GRANT OF LICENSE. This EULA grants you the following rights:
12
+·	Installation and Use. You may install and use an unlimited number of copies of the SOFTWARE PRODUCT.
13
+·	Reproduction and Distribution. You may reproduce and distribute an unlimited number of copies of the SOFTWARE PRODUCT;  provided that each copy shall be a true and complete copy, including all copyright and trademark notices, and shall be accompanied by a copy of this EULA.  Copies of the SOFTWARE PRODUCT may not be distributed for profit either on a standalone basis or included as part of your own product.
14
+2.	DESCRIPTION OF OTHER RIGHTS AND LIMITATIONS.
15
+·	Limitations on Reverse Engineering, Decompilation, and Disassembly. You may not reverse engineer, decompile, or disassemble the SOFTWARE PRODUCT, except and only to the extent that such activity is expressly permitted by applicable law notwithstanding this limitation.
16
+· Restrictions on Alteration.  You may not rename, edit or create any derivative works from the SOFTWARE PRODUCT, other than subsetting when embedding them in documents.
17
+·	Software Transfer. You may permanently transfer all of your rights under this EULA, provided the recipient agrees to the terms of this EULA.
18
+·	Termination. Without prejudice to any other rights, Microsoft may terminate this EULA if you fail to comply with the terms and conditions of this EULA. In such event, you must destroy all copies of the SOFTWARE PRODUCT and all of its component parts.
19
+3. COPYRIGHT. All title and copyrights in and to the SOFTWARE PRODUCT (including but not limited to any images, text, and "applets" incorporated into the SOFTWARE PRODUCT), the accompanying printed materials, and any copies of the SOFTWARE PRODUCT are owned by Microsoft or its suppliers. The SOFTWARE PRODUCT is protected by copyright laws and international treaty provisions. Therefore, you must treat the SOFTWARE PRODUCT like any other copyrighted material.
20
+4.	U.S. GOVERNMENT RESTRICTED RIGHTS. The SOFTWARE PRODUCT and documentation are provided with RESTRICTED RIGHTS. Use, duplication, or disclosure by the Government is subject to restrictions as set forth in subparagraph (c)(1)(ii) of the Rights in Technical Data and Computer Software clause at DFARS 252.227-7013 or subparagraphs (c)(1) and (2) of the Commercial Computer Software-Restricted Rights at 48 CFR 52.227-19, as applicable. Manufacturer is Microsoft Corporation/One Microsoft Way/Redmond, WA 98052-6399.
21
+LIMITED WARRANTY
22
+NO WARRANTIES. Microsoft expressly disclaims any warranty for the SOFTWARE PRODUCT. The SOFTWARE PRODUCT and any related documentation is provided "as is" without warranty of any kind, either express or implied, including, without limitation, the implied warranties or merchantability, fitness for a particular purpose, or noninfringement. The entire risk arising out of use or performance of the SOFTWARE PRODUCT remains with you.
23
+NO LIABILITY FOR CONSEQUENTIAL DAMAGES. In no event shall Microsoft or its suppliers be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or any other pecuniary loss) arising out of the use of or inability to use this Microsoft product, even if Microsoft has been advised of the possibility of such damages. Because some states/jurisdictions do not allow the exclusion or limitation of liability for consequential or incidental damages, the above limitation may not apply to you.
24
+MISCELLANEOUS
25
+If you acquired this product in the United States, this EULA is governed by the laws of the State of Washington.
26
+If this product was acquired outside the United States, then local laws may apply.
27
+Should you have any questions concerning this EULA, or if you desire to contact Microsoft for any reason, please contact the Microsoft subsidiary serving your country, or write: Microsoft Sales Information Center/One Microsoft Way/Redmond, WA  98052-6399.
28
+
29
+
30
+
31
+
32
+
33
+
34
+INSTALLING FONTS
35
+________________
36
+----------------
37
+
38
+
39
+Windows95
40
+---------
41
+
42
+To add a new font to your computer:
43
+
44
+1) Open the font folder from the Control Panel.
45
+
46
+2) On the File menu, click Install New Font.
47
+
48
+3) Click the drive and folder that contain the fonts you want to add.
49
+
50
+4) Double-click the icon for the font you want to add.
51
+
52
+
53
+Tips
54
+
55
+To select more than one font to add, press and hold down the CTRL key, and then click the fonts you want.
56
+
57
+To select a range of fonts in the list, press and hold down the SHIFT key while dragging the cursor over the fonts.
58
+
59
+To add fonts from a network drive without using disk space on your computer, make sure Copy Fonts To Windows Folder is not checked.
60
+
61
+
62
+
63
+
64
+
65
+Windows 3.1x
66
+------------
67
+
68
+To add fonts to your computer:
69
+
70
+1) In the Control Panel window, choose the Fonts icon. The fonts already installed are listed in the Installed Fonts box.
71
+
72
+2) Choose the Add button.
73
+
74
+3) In the Add Fonts dialog box, select the font you want to add. You can add more than one font at a time. You can also select all the fonts listed by choosing the Select All button. If the font is not located in the current drive or directory, select the drive and directory where it is located. If the font you want to add is on someone else's computer, you can use the Network button to browse through and connect to shared directories on other computers.
75
+
76
+4) If you are low on disk space and want to use the fonts directly from the directory (network or local) where they are located, without copying the font files to the Windows directory, clear the Copy Fonts To Windows Directory check box.
77
+
78
+5) Choose the OK button.
79
+
80
+6) In the Fonts dialog box, choose the Close button.
81
+
82
+For help with the Fonts and Add Fonts dialog boxes, choose the Help button or press F1 while using the dialog boxes.
83
+
84
+
85
+
86
+
87
+Windows NT
88
+----------
89
+
90
+To add fonts to your computer:
91
+
92
+1) In the Control Panel window, choose the Fonts icon. The fonts already installed are listed in the Installed Fonts box.
93
+
94
+2) Choose the Add button.
95
+
96
+3)In the Add Fonts dialog box, select the font you want to add. If the font is not located in the current drive or directory, select the drive and directory where it is located. You can add more than one font at a time. You can also select all the fonts listed by choosing the Select All button.
97
+
98
+4) If you are low on disk space and want to use the fonts directly from the directory (network or local) where they are located, without copying the font files to the \systemroot\SYSTEM directory, clear the Copy Fonts To Windows Directory check box.
99
+
100
+5) Choose the OK button.
101
+
102
+6) Choose the Close button.
103
+
104
+For help with the Fonts and Add Fonts dialog boxes, choose the Help button or press F1 while using the dialog boxes.
105
+
106
+
107
+

BIN
classes/fonts/TREBUC.TTF View File


BIN
classes/fonts/TREBUCBD.TTF View File


BIN
classes/fonts/TREBUCBI.TTF View File


BIN
classes/fonts/TREBUCIT.TTF View File


BIN
classes/fonts/VERDANA.TTF View File


BIN
classes/fonts/VERDANAB.TTF View File


BIN
classes/fonts/VERDANAI.TTF View File


BIN
classes/fonts/VERDANAZ.TTF View File


+ 580
- 0
classes/format.class.php View File

1
+<?
2
+class Format {
3
+	/**
4
+	 * Torrent Labels
5
+	 * Map a common display string to a CSS class
6
+	 * Indexes are lower case
7
+	 * Note the "tl_" prefix for "torrent label"
8
+	 *
9
+	 * There are five basic types:
10
+	 * * tl_free (leech status)
11
+	 * * tl_snatched
12
+	 * * tl_reported
13
+	 * * tl_approved
14
+	 * * tl_notice (default)
15
+	 *
16
+	 * @var array Strings
17
+	 */
18
+	private static $TorrentLabels = array(
19
+		'default'  => 'tl_notice',
20
+		'snatched' => 'tl_snatched',
21
+		'seeding'  => 'tl_seeding',
22
+		'leeching' => 'tl_leeching',
23
+
24
+		'freeleech'			=> 'tl_free',
25
+		'neutral leech'		=> 'tl_free tl_neutral',
26
+		'personal freeleech'=> 'tl_free tl_personal',
27
+
28
+		'reported'		=> 'tl_reported',
29
+		'bad tags'		=> 'tl_reported tl_bad_tags',
30
+		'bad folders'	=> 'tl_reported tl_bad_folders',
31
+		'bad file names'=> 'tl_reported tl_bad_file_names',
32
+
33
+		'uncensored'	=> 'tl_notice'
34
+	);
35
+
36
+	/**
37
+	 * Shorten a string
38
+	 *
39
+	 * @param $Str string to cut
40
+	 * @param $Length cut at length
41
+	 * @param $Hard force cut at length instead of at closest word
42
+	 * @param $ShowDots Show dots at the end
43
+	 * @return string formatted string
44
+	 */
45
+	public static function cut_string($Str, $Length, $Hard = false, $ShowDots = true) {
46
+		if (mb_strlen($Str, 'UTF-8') > $Length) {
47
+			if ($Hard == 0) {
48
+				// Not hard, cut at closest word
49
+				$CutDesc = mb_substr($Str, 0, $Length, 'UTF-8');
50
+				$DescArr = explode(' ', $CutDesc);
51
+				if (count($DescArr) > 1) {
52
+					array_pop($DescArr);
53
+					$CutDesc = implode(' ', $DescArr);
54
+				}
55
+				if ($ShowDots) {
56
+					$CutDesc .= '...';
57
+				}
58
+			} else {
59
+				$CutDesc = mb_substr($Str, 0, $Length, 'UTF-8');
60
+				if ($ShowDots) {
61
+					$CutDesc .= '...';
62
+				}
63
+			}
64
+			return $CutDesc;
65
+		} else {
66
+			return $Str;
67
+		}
68
+	}
69
+
70
+
71
+	/**
72
+	 * Gets the CSS class corresponding to a ratio
73
+	 *
74
+	 * @param $Ratio ratio to get the css class for
75
+	 * @return string the CSS class corresponding to the ratio range
76
+	 */
77
+	public static function get_ratio_color($Ratio) {
78
+		if ($Ratio < 0.1) { return 'r00'; }
79
+		if ($Ratio < 0.2) { return 'r01'; }
80
+		if ($Ratio < 0.3) { return 'r02'; }
81
+		if ($Ratio < 0.4) { return 'r03'; }
82
+		if ($Ratio < 0.5) { return 'r04'; }
83
+		if ($Ratio < 0.6) { return 'r05'; }
84
+		if ($Ratio < 0.7) { return 'r06'; }
85
+		if ($Ratio < 0.8) { return 'r07'; }
86
+		if ($Ratio < 0.9) { return 'r08'; }
87
+		if ($Ratio < 1) { return 'r09'; }
88
+		if ($Ratio < 2) { return 'r10'; }
89
+		if ($Ratio < 5) { return 'r20'; }
90
+		return 'r50';
91
+	}
92
+
93
+
94
+	/**
95
+	 * Calculates and formats a ratio.
96
+	 *
97
+	 * @param int $Dividend AKA numerator
98
+	 * @param int $Divisor
99
+	 * @param boolean $Color if true, ratio will be coloured.
100
+	 * @return string formatted ratio HTML
101
+	 */
102
+	public static function get_ratio_html($Dividend, $Divisor, $Color = true) {
103
+		$Ratio = self::get_ratio($Dividend, $Divisor);
104
+
105
+		if ($Ratio === false) {
106
+			return '--';
107
+		}
108
+		if ($Ratio === '∞') {
109
+			return '<span class="tooltip r99" title="Infinite">∞</span>';
110
+		}
111
+		if ($Color) {
112
+			$Ratio = sprintf('<span class="tooltip %s" title="%s">%s</span>',
113
+				self::get_ratio_color($Ratio),
114
+				self::get_ratio($Dividend, $Divisor, 5),
115
+				$Ratio
116
+			);
117
+		}
118
+
119
+		return $Ratio;
120
+	}
121
+
122
+	/**
123
+	 * Returns ratio
124
+	 * @param int $Dividend
125
+	 * @param int $Divisor
126
+	 * @param int $Decimal floor to n decimals (e.g. subtract .005 to floor to 2 decimals)
127
+	 * @return boolean|string
128
+	 */
129
+	public static function get_ratio($Dividend, $Divisor, $Decimal = 2) {
130
+		if ($Divisor == 0 && $Dividend == 0) {
131
+			return false;
132
+		}
133
+		if ($Divisor == 0) {
134
+			return '∞';
135
+		}
136
+		return number_format(max($Dividend / $Divisor - (0.5 / pow(10, $Decimal)), 0), $Decimal);
137
+	}
138
+
139
+	/**
140
+	 * Gets the query string of the current page, minus the parameters in $Exclude
141
+	 *
142
+	 * @param array $Exclude Query string parameters to leave out, or blank to include all parameters.
143
+	 * @param bool $Escape Whether to return a string prepared for HTML output
144
+	 * @param bool $Sort Whether to sort the parameters by key
145
+	 * @return An optionally HTML sanatized query string
146
+	 */
147
+	public static function get_url($Exclude = false, $Escape = true, $Sort = false) {
148
+		if ($Exclude !== false) {
149
+			$Separator = $Escape ? '&amp;' : '&';
150
+			$QueryItems = NULL;
151
+			parse_str($_SERVER['QUERY_STRING'], $QueryItems);
152
+			foreach ($Exclude as $Key) {
153
+				unset($QueryItems[$Key]);
154
+			}
155
+			if ($Sort) {
156
+				ksort($QueryItems);
157
+			}
158
+			return http_build_query($QueryItems, '', $Separator);
159
+		} else {
160
+			return $Escape ? display_str($_SERVER['QUERY_STRING']) : $_SERVER['QUERY_STRING'];
161
+		}
162
+	}
163
+
164
+
165
+	/**
166
+	 * Finds what page we're on and gives it to us, as well as the LIMIT clause for SQL
167
+	 * Takes in $_GET['page'] as an additional input
168
+	 *
169
+	 * @param $PerPage Results to show per page
170
+	 * @param $DefaultResult Optional, which result's page we want if no page is specified
171
+	 * If this parameter is not specified, we will default to page 1
172
+	 *
173
+	 * @return array(int, string) What page we are on, and what to use in the LIMIT section of a query
174
+	 * e.g. "SELECT [...] LIMIT $Limit;"
175
+	 */
176
+	public static function page_limit($PerPage, $DefaultResult = 1) {
177
+		if (!isset($_GET['page'])) {
178
+			$Page = ceil($DefaultResult / $PerPage);
179
+			if ($Page == 0) {
180
+				$Page = 1;
181
+			}
182
+			$Limit = $PerPage;
183
+		} else {
184
+			if (!is_number($_GET['page'])) {
185
+				error(0);
186
+			}
187
+			$Page = $_GET['page'];
188
+			if ($Page <= 0) {
189
+				$Page = 1;
190
+			}
191
+			$Limit = $PerPage * $Page - $PerPage . ", $PerPage";
192
+		}
193
+		return array($Page, $Limit);
194
+	}
195
+
196
+	// A9 magic. Some other poor soul can write the phpdoc.
197
+	// For data stored in memcached catalogues (giant arrays), e.g. forum threads
198
+	public static function catalogue_limit($Page, $PerPage, $CatalogueSize = 500) {
199
+		$CatalogueID = floor(($PerPage * $Page - $PerPage) / $CatalogueSize);
200
+		$CatalogueLimit = ($CatalogueID * $CatalogueSize).", $CatalogueSize";
201
+		return array($CatalogueID, $CatalogueLimit);
202
+	}
203
+
204
+	public static function catalogue_select($Catalogue, $Page, $PerPage, $CatalogueSize = 500) {
205
+		return array_slice($Catalogue, (($PerPage * $Page - $PerPage) % $CatalogueSize), $PerPage, true);
206
+	}
207
+
208
+
209
+	/* Get pages
210
+	 * Returns a page list, given certain information about the pages.
211
+	 *
212
+	 * @param int $StartPage: The current record the page you're on starts with.
213
+	 *		e.g. if you're on page 2 of a forum thread with 25 posts per page, $StartPage is 25.
214
+	 *		If you're on page 1, $StartPage is 0.
215
+	 * @param int $TotalRecords: The total number of records in the result set.
216
+	 *		e.g. if you're on a forum thread with 152 posts, $TotalRecords is 152.
217
+	 * @param int $ItemsPerPage: Self-explanatory. The number of records shown on each page
218
+	 *		e.g. if there are 25 posts per forum page, $ItemsPerPage is 25.
219
+	 * @param int $ShowPages: The number of page links that are shown.
220
+	 *		e.g. If there are 20 pages that exist, but $ShowPages is only 11, only 11 links will be shown.
221
+	 * @param string $Anchor A URL fragment to attach to the links.
222
+	 *		e.g. '#comment12'
223
+	 * @return A sanitized HTML page listing.
224
+	 */
225
+	public static function get_pages($StartPage, $TotalRecords, $ItemsPerPage, $ShowPages = 11, $Anchor = '') {
226
+		global $Document, $Method, $Mobile;
227
+		$Location = "$Document.php";
228
+		$StartPage = ceil($StartPage);
229
+		$TotalPages = 0;
230
+		if ($TotalRecords > 0) {
231
+			$StartPage = min($StartPage, ceil($TotalRecords / $ItemsPerPage));
232
+
233
+			$ShowPages--;
234
+			$TotalPages = ceil($TotalRecords / $ItemsPerPage);
235
+
236
+			if ($TotalPages > $ShowPages) {
237
+				$StartPosition = $StartPage - round($ShowPages / 2);
238
+
239
+				if ($StartPosition <= 0) {
240
+					$StartPosition = 1;
241
+				} else {
242
+					if ($StartPosition >= ($TotalPages - $ShowPages)) {
243
+						$StartPosition = $TotalPages - $ShowPages;
244
+					}
245
+				}
246
+
247
+				$StopPage = $ShowPages + $StartPosition;
248
+
249
+			} else {
250
+				$StopPage = $TotalPages;
251
+				$StartPosition = 1;
252
+			}
253
+
254
+			$StartPosition = max($StartPosition, 1);
255
+
256
+			$QueryString = self::get_url(array('page', 'post'));
257
+			if ($QueryString != '') {
258
+				$QueryString = "&amp;$QueryString";
259
+			}
260
+
261
+			$Pages = '';
262
+
263
+			if ($StartPage > 1) {
264
+				$Pages .= "<a href=\"$Location?page=1$QueryString$Anchor\"><strong>&lt;&lt; First</strong></a> ";
265
+				$Pages .= "<a href=\"$Location?page=".($StartPage - 1).$QueryString.$Anchor.'" class="pager_prev"><strong>&lt; Prev</strong></a> | ';
266
+			}
267
+			//End change
268
+
269
+			if (!$Mobile) {
270
+				for ($i = $StartPosition; $i <= $StopPage; $i++) {
271
+					if ($i != $StartPage) {
272
+						$Pages .= "<a href=\"$Location?page=$i$QueryString$Anchor\">";
273
+					}
274
+					$Pages .= '<strong>';
275
+					if ($i * $ItemsPerPage > $TotalRecords) {
276
+						$Pages .= ((($i - 1) * $ItemsPerPage) + 1)."-$TotalRecords";
277
+					} else {
278
+						$Pages .= ((($i - 1) * $ItemsPerPage) + 1).'-'.($i * $ItemsPerPage);
279
+					}
280
+
281
+					$Pages .= '</strong>';
282
+					if ($i != $StartPage) {
283
+						$Pages .= '</a>';
284
+					}
285
+					if ($i < $StopPage) {
286
+						$Pages .= ' | ';
287
+					}
288
+				}
289
+			} else {
290
+				$Pages .= $StartPage;
291
+			}
292
+
293
+			if ($StartPage && $StartPage < $TotalPages) {
294
+				$Pages .= " | <a href=\"$Location?page=".($StartPage + 1).$QueryString.$Anchor.'" class="pager_next"><strong>Next &gt;</strong></a> ';
295
+				$Pages .= "<a href=\"$Location?page=$TotalPages$QueryString$Anchor\"><strong> Last &gt;&gt;</strong></a>";
296
+			}
297
+		}
298
+		if ($TotalPages > 1) {
299
+			return $Pages;
300
+		}
301
+	}
302
+
303
+
304
+	/**
305
+	 * Format a size in bytes as a human readable string in KiB/MiB/...
306
+	 * Note: KiB, MiB, etc. are the IEC units, which are in base 2.
307
+	 * KB, MB are the SI units, which are in base 10.
308
+	 *
309
+	 * @param int $Size
310
+	 * @param int $Levels Number of decimal places. Defaults to 2, unless the size >= 1TB, in which case it defaults to 4.
311
+	 * @return string formatted number.
312
+	 */
313
+	public static function get_size($Size, $Levels = 2) {
314
+		$Units = array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB');
315
+		$Size = (double)$Size;
316
+		for ($Steps = 0; abs($Size) >= 1024 && $Steps < count($Units); $Size /= 1024, $Steps++) { }
317
+		if (func_num_args() == 1 && $Steps >= 4) {
318
+			$Levels++;
319
+		}
320
+		return number_format($Size, $Levels) . ' ' . $Units[$Steps];
321
+	}
322
+
323
+
324
+	/**
325
+	 * Format a number as a multiple of its highest power of 1000 (e.g. 10035 -> '10.04k')
326
+	 *
327
+	 * @param int $Number
328
+	 * @return string formatted number.
329
+	 */
330
+	public static function human_format($Number) {
331
+		$Steps = 0;
332
+		while ($Number >= 1000) {
333
+			$Steps++;
334
+			$Number = $Number / 1000;
335
+		}
336
+		switch ($Steps) {
337
+			case 0: return round($Number); break;
338
+			case 1: return round($Number, 2).'k'; break;
339
+			case 2: return round($Number, 2).'M'; break;
340
+			case 3: return round($Number, 2).'G'; break;
341
+			case 4: return round($Number, 2).'T'; break;
342
+			case 5: return round($Number, 2).'P'; break;
343
+			default:
344
+				return round($Number, 2).'E + '.$Steps * 3;
345
+		}
346
+	}
347
+
348
+
349
+	/**
350
+	 * Given a formatted string of a size, get the number of bytes it represents.
351
+	 *
352
+	 * @param string $Size formatted size string, e.g. 123.45k
353
+	 * @return Number of bytes it represents, e.g. (123.45 * 1024)
354
+	 */
355
+	public static function get_bytes($Size) {
356
+		list($Value, $Unit) = sscanf($Size, "%f%s");
357
+		$Unit = ltrim($Unit);
358
+		if (empty($Unit)) {
359
+			return $Value ? round($Value) : 0;
360
+		}
361
+		switch (strtolower($Unit[0])) {
362
+			case 'k': return round($Value * 1024);
363
+			case 'm': return round($Value * 1048576);
364
+			case 'g': return round($Value * 1073741824);
365
+			case 't': return round($Value * 1099511627776);
366
+			default: return 0;
367
+		}
368
+	}
369
+
370
+
371
+	/**
372
+	 * Reverse the effects of display_str - un-sanitize HTML.
373
+	 * Use sparingly.
374
+	 *
375
+	 * @param string $Str the string to unsanitize
376
+	 * @return unsanitized string
377
+	 */
378
+	// Use sparingly
379
+	public static function undisplay_str($Str) {
380
+		return mb_convert_encoding($Str, 'UTF-8', 'HTML-ENTITIES');
381
+	}
382
+
383
+
384
+	/**
385
+	 * Echo data sent in a GET form field, useful for text areas.
386
+	 *
387
+	 * @param string $Index the name of the form field
388
+	 * @param boolean $Return if set to true, value is returned instead of echoed.
389
+	 * @return Sanitized value of field index if $Return == true
390
+	 */
391
+	public static function form($Index, $Return = false) {
392
+		if (!empty($_GET[$Index])) {
393
+			if ($Return) {
394
+				return display_str($_GET[$Index]);
395
+			} else {
396
+				echo display_str($_GET[$Index]);
397
+			}
398
+		}
399
+	}
400
+
401
+
402
+	/**
403
+	 * Convenience function to echo out selected="selected" and checked="checked" so you don't have to.
404
+	 *
405
+	 * @param string $Name the name of the option in the select (or field in $Array)
406
+	 * @param mixed $Value the value that the option must be for the option to be marked as selected or checked
407
+	 * @param string $Attribute The value returned/echoed is $Attribute="$Attribute" with a leading space
408
+	 * @param array $Array The array the option is in, defaults to GET.
409
+	 * @return void
410
+	 */
411
+	public static function selected($Name, $Value, $Attribute = 'selected', $Array = array()) {
412
+		if (empty($Array)) {
413
+			$Array = $_GET;
414
+		}
415
+		if (isset($Array[$Name]) && $Array[$Name] !== '') {
416
+			if ($Array[$Name] == $Value) {
417
+				echo " $Attribute=\"$Attribute\"";
418
+			}
419
+		}
420
+	}
421
+
422
+	/**
423
+	 * Return a CSS class name if certain conditions are met. Mainly useful to mark links as 'active'
424
+	 *
425
+	 * @param mixed $Target The variable to compare all values against
426
+	 * @param mixed $Tests The condition values. Type and dimension determines test type
427
+	 *                 Scalar: $Tests must be equal to $Target for a match
428
+	 *                 Vector: All elements in $Tests must correspond to equal values in $Target
429
+	 *                 2-dimensional array: At least one array must be identical to $Target
430
+	 * @param string $ClassName CSS class name to return
431
+	 * @param bool $AddAttribute Whether to include the "class" attribute in the output
432
+	 * @param string $UserIDKey Key in _REQUEST for a user ID parameter, which if given will be compared to G::$LoggedUser[ID]
433
+	 *
434
+	 * @return class name on match, otherwise an empty string
435
+	 */
436
+	public static function add_class($Target, $Tests, $ClassName, $AddAttribute, $UserIDKey = false) {
437
+		if ($UserIDKey && isset($_REQUEST[$UserIDKey]) && G::$LoggedUser['ID'] != $_REQUEST[$UserIDKey]) {
438
+			return '';
439
+		}
440
+		$Pass = true;
441
+		if (!is_array($Tests)) {
442
+			// Scalars are nice and easy
443
+			$Pass = $Tests === $Target;
444
+		} elseif (!is_array($Tests[0])) {
445
+			// Test all values in vectors
446
+			foreach ($Tests as $Type => $Part) {
447
+				if (!isset($Target[$Type]) || $Target[$Type] !== $Part) {
448
+					$Pass = false;
449
+					break;
450
+				}
451
+			}
452
+		} else {
453
+			// Loop to the end of the array or until we find a matching test
454
+			foreach ($Tests as $Test) {
455
+				$Pass = true;
456
+				// If $Pass remains true after this test, it's a match
457
+				foreach ($Test as $Type => $Part) {
458
+					if (!isset($Target[$Type]) || $Target[$Type] !== $Part) {
459
+						$Pass = false;
460
+						break;
461
+					}
462
+				}
463
+				if ($Pass) {
464
+					break;
465
+				}
466
+			}
467
+		}
468
+		if (!$Pass) {
469
+			return '';
470
+		}
471
+		if ($AddAttribute) {
472
+			return " class=\"$ClassName\"";
473
+		}
474
+		return " $ClassName";
475
+	}
476
+
477
+
478
+	/**
479
+	 * Detect the encoding of a string and transform it to UTF-8.
480
+	 *
481
+	 * @param string $Str
482
+	 * @return UTF-8 encoded version of $Str
483
+	 */
484
+	public static function make_utf8($Str) {
485
+		if ($Str != '') {
486
+			if (self::is_utf8($Str)) {
487
+				$Encoding = 'UTF-8';
488
+			}
489
+			if (empty($Encoding)) {
490
+				$Encoding = mb_detect_encoding($Str, 'UTF-8, ISO-8859-1');
491
+			}
492
+			if (empty($Encoding)) {
493
+				$Encoding = 'ISO-8859-1';
494
+			}
495
+			if ($Encoding == 'UTF-8') {
496
+				return $Str;
497
+			} else {
498
+				return @mb_convert_encoding($Str, 'UTF-8', $Encoding);
499
+			}
500
+		}
501
+	}
502
+
503
+	/**
504
+	 * Magical function.
505
+	 *
506
+	 * @param string $Str function to detect encoding on.
507
+	 * @return true if the string is in UTF-8.
508
+	 */
509
+	public static function is_utf8($Str) {
510
+		return preg_match('%^(?:
511
+			[\x09\x0A\x0D\x20-\x7E]              // ASCII
512
+			| [\xC2-\xDF][\x80-\xBF]             // non-overlong 2-byte
513
+			| \xE0[\xA0-\xBF][\x80-\xBF]         // excluding overlongs
514
+			| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}  // straight 3-byte
515
+			| \xED[\x80-\x9F][\x80-\xBF]         // excluding surrogates
516
+			| \xF0[\x90-\xBF][\x80-\xBF]{2}      // planes 1-3
517
+			| [\xF1-\xF3][\x80-\xBF]{3}          // planes 4-15
518
+			| \xF4[\x80-\x8F][\x80-\xBF]{2}      // plane 16
519
+			)*$%xs', $Str
520
+		);
521
+	}
522
+
523
+	/**
524
+	 * Modified accessor for the $TorrentLabels array
525
+	 *
526
+	 * Converts $Text to lowercase and strips non-word characters
527
+	 *
528
+	 * @param string $Text Search string
529
+	 * @return string CSS class(es)
530
+	 */
531
+	public static function find_torrent_label_class($Text) {
532
+		$Index = mb_eregi_replace('(?:[^\w\d\s]+)', '', strtolower($Text));
533
+		if (isset(self::$TorrentLabels[$Index])) {
534
+			return self::$TorrentLabels[$Index];
535
+		} else {
536
+			return self::$TorrentLabels['default'];
537
+		}
538
+	}
539
+
540
+	/**
541
+	 * Creates a strong element that notes the torrent's state.
542
+	 * E.g.: snatched/freeleech/neutral leech/reported
543
+	 *
544
+	 * The CSS class is inferred using find_torrent_label_class($Text)
545
+	 *
546
+	 * @param string $Text Display text
547
+	 * @param string $Class Custom CSS class
548
+	 * @return string <strong> element
549
+	 */
550
+	public static function torrent_label($Text, $Class = '') {
551
+		if (empty($Class)) {
552
+			$Class = self::find_torrent_label_class($Text);
553
+		}
554
+		return sprintf('<strong class="torrent_label tooltip %1$s" title="%2$s" style="white-space: nowrap;">%2$s</strong>',
555
+				display_str($Class), display_str($Text));
556
+	}
557
+
558
+	/**
559
+	 * Formats a CSS class name from a Category ID
560
+	 * @global array $Categories
561
+	 * @param int|string $CategoryID This number will be subtracted by one
562
+	 * @return string
563
+	 */
564
+	public static function css_category($CategoryID = 1) {
565
+		global $Categories;
566
+		return 'cats_' . strtolower(str_replace(array('-', ' '), '',
567
+				$Categories[$CategoryID - 1]));
568
+	}
569
+
570
+	/**
571
+	 * Formats a CSS class name from a Category ID
572
+	 * @global array $Categories
573
+	 * @param int|string $CategoryID This number will be subtracted by one
574
+	 * @return string
575
+	 */
576
+	public static function pretty_category($CategoryID = 1) {
577
+		global $Categories;
578
+		return ucwords(str_replace('-', ' ', $Categories[$CategoryID - 1]));
579
+	}
580
+}

+ 325
- 0
classes/forums.class.php View File

1
+<?
2
+class Forums {
3
+	/**
4
+	 * Get information on a thread.
5
+	 *
6
+	 * @param int $ThreadID
7
+	 *        	the thread ID.
8
+	 * @param boolean $Return
9
+	 *        	indicates whether thread info should be returned.
10
+	 * @param Boolean $SelectiveCache
11
+	 *        	cache thread info/
12
+	 * @return array holding thread information.
13
+	 */
14
+	public static function get_thread_info($ThreadID, $Return = true, $SelectiveCache = false) {
15
+		if ((!$ThreadInfo = G::$Cache->get_value('thread_' . $ThreadID . '_info')) || !isset($ThreadInfo['Ranking'])) {
16
+			$QueryID = G::$DB->get_query_id();
17
+			G::$DB->query("
18
+				SELECT
19
+					t.Title,
20
+					t.ForumID,
21
+					t.IsLocked,
22
+					t.IsSticky,
23
+					COUNT(fp.id) AS Posts,
24
+					t.LastPostAuthorID,
25
+					ISNULL(p.TopicID) AS NoPoll,
26
+					t.StickyPostID,
27
+					t.AuthorID as OP,
28
+					t.Ranking
29
+				FROM forums_topics AS t
30
+					JOIN forums_posts AS fp ON fp.TopicID = t.ID
31
+					LEFT JOIN forums_polls AS p ON p.TopicID = t.ID
32
+				WHERE t.ID = '$ThreadID'
33
+				GROUP BY fp.TopicID");
34
+			if (!G::$DB->has_results()) {
35
+				G::$DB->set_query_id($QueryID);
36
+				return null;
37
+			}
38
+			$ThreadInfo = G::$DB->next_record(MYSQLI_ASSOC, false);
39
+			if ($ThreadInfo['StickyPostID']) {
40
+				$ThreadInfo['Posts']--;
41
+				G::$DB->query(
42
+					"SELECT
43
+						p.ID,
44
+						p.AuthorID,
45
+						p.AddedTime,
46
+						p.Body,
47
+						p.EditedUserID,
48
+						p.EditedTime,
49
+						ed.Username
50
+						FROM forums_posts AS p
51
+							LEFT JOIN users_main AS ed ON ed.ID = p.EditedUserID
52
+						WHERE p.TopicID = '$ThreadID'
53
+							AND p.ID = '" . $ThreadInfo['StickyPostID'] . "'");
54
+				list ($ThreadInfo['StickyPost']) = G::$DB->to_array(false, MYSQLI_ASSOC);
55
+			}
56
+			G::$DB->set_query_id($QueryID);
57
+			if (!$SelectiveCache || !$ThreadInfo['IsLocked'] || $ThreadInfo['IsSticky']) {
58
+				G::$Cache->cache_value('thread_' . $ThreadID . '_info', $ThreadInfo, 0);
59
+			}
60
+		}
61
+		if ($Return) {
62
+			return $ThreadInfo;
63
+		}
64
+	}
65
+
66
+	/**
67
+	 * Checks whether user has permissions on a forum.
68
+	 *
69
+	 * @param int $ForumID
70
+	 *        	the forum ID.
71
+	 * @param string $Perm
72
+	 *        	the permissision to check, defaults to 'Read'
73
+	 * @return boolean true if user has permission
74
+	 */
75
+	public static function check_forumperm($ForumID, $Perm = 'Read') {
76
+		$Forums = self::get_forums();
77
+		if (isset(G::$LoggedUser['CustomForums'][$ForumID]) && G::$LoggedUser['CustomForums'][$ForumID] == 1) {
78
+			return true;
79
+		}
80
+		if ($ForumID == DONOR_FORUM && Donations::has_donor_forum(G::$LoggedUser['ID'])) {
81
+			return true;
82
+		}
83
+		if ($Forums[$ForumID]['MinClass' . $Perm] > G::$LoggedUser['Class'] && (!isset(G::$LoggedUser['CustomForums'][$ForumID]) || G::$LoggedUser['CustomForums'][$ForumID] == 0)) {
84
+			return false;
85
+		}
86
+		if (isset(G::$LoggedUser['CustomForums'][$ForumID]) && G::$LoggedUser['CustomForums'][$ForumID] == 0) {
87
+			return false;
88
+		}
89
+		return true;
90
+	}
91
+
92
+	/**
93
+	 * Gets basic info on a forum.
94
+	 *
95
+	 * @param int $ForumID
96
+	 *        	the forum ID.
97
+	 */
98
+	public static function get_forum_info($ForumID) {
99
+		$Forum = G::$Cache->get_value("ForumInfo_$ForumID");
100
+		if (!$Forum) {
101
+			$QueryID = G::$DB->get_query_id();
102
+			G::$DB->query("
103
+				SELECT
104
+					Name,
105
+					MinClassRead,
106
+					MinClassWrite,
107
+					MinClassCreate,
108
+					COUNT(forums_topics.ID) AS Topics
109
+				FROM forums
110
+					LEFT JOIN forums_topics ON forums_topics.ForumID = forums.ID
111
+				WHERE forums.ID = '$ForumID'
112
+				GROUP BY ForumID");
113
+			if (!G::$DB->has_results()) {
114
+				return false;
115
+			}
116
+			// Makes an array, with $Forum['Name'], etc.
117
+			$Forum = G::$DB->next_record(MYSQLI_ASSOC);
118
+
119
+			G::$DB->set_query_id($QueryID);
120
+
121
+			G::$Cache->cache_value("ForumInfo_$ForumID", $Forum, 86400);
122
+		}
123
+		return $Forum;
124
+	}
125
+
126
+	/**
127
+	 * Get the forum categories
128
+	 * @return array ForumCategoryID => Name
129
+	 */
130
+	public static function get_forum_categories() {
131
+		$ForumCats = G::$Cache->get_value('forums_categories');
132
+		if ($ForumCats === false) {
133
+			$QueryID = G::$DB->get_query_id();
134
+			G::$DB->query("
135
+				SELECT ID, Name
136
+				FROM forums_categories");
137
+			$ForumCats = array();
138
+			while (list ($ID, $Name) = G::$DB->next_record()) {
139
+				$ForumCats[$ID] = $Name;
140
+			}
141
+			G::$DB->set_query_id($QueryID);
142
+			G::$Cache->cache_value('forums_categories', $ForumCats, 0);
143
+		}
144
+		return $ForumCats;
145
+	}
146
+
147
+	/**
148
+	 * Get the forums
149
+	 * @return array ForumID => (various information about the forum)
150
+	 */
151
+	public static function get_forums() {
152
+		if (!$Forums = G::$Cache->get_value('forums_list')) {
153
+			$QueryID = G::$DB->get_query_id();
154
+			G::$DB->query("
155
+				SELECT
156
+					f.ID,
157
+					f.CategoryID,
158
+					f.Name,
159
+					f.Description,
160
+					f.MinClassRead AS MinClassRead,
161
+					f.MinClassWrite AS MinClassWrite,
162
+					f.MinClassCreate AS MinClassCreate,
163
+					f.NumTopics,
164
+					f.NumPosts,
165
+					f.LastPostID,
166
+					f.LastPostAuthorID,
167
+					f.LastPostTopicID,
168
+					f.LastPostTime,
169
+					0 AS SpecificRules,
170
+					t.Title,
171
+					t.IsLocked AS Locked,
172
+					t.IsSticky AS Sticky
173
+				FROM forums AS f
174
+					JOIN forums_categories AS fc ON fc.ID = f.CategoryID
175
+					LEFT JOIN forums_topics AS t ON t.ID = f.LastPostTopicID
176
+				GROUP BY f.ID
177
+				ORDER BY fc.Sort, fc.Name, f.CategoryID, f.Sort");
178
+			$Forums = G::$DB->to_array('ID', MYSQLI_ASSOC, false);
179
+
180
+			G::$DB->query("
181
+				SELECT ForumID, ThreadID
182
+				FROM forums_specific_rules");
183
+			$SpecificRules = array();
184
+			while (list($ForumID, $ThreadID) = G::$DB->next_record(MYSQLI_NUM, false)) {
185
+				$SpecificRules[$ForumID][] = $ThreadID;
186
+			}
187
+			G::$DB->set_query_id($QueryID);
188
+			foreach ($Forums as $ForumID => &$Forum) {
189
+				if (isset($SpecificRules[$ForumID])) {
190
+					$Forum['SpecificRules'] = $SpecificRules[$ForumID];
191
+				} else {
192
+					$Forum['SpecificRules'] = array();
193
+				}
194
+			}
195
+			G::$Cache->cache_value('forums_list', $Forums, 0);
196
+		}
197
+		return $Forums;
198
+	}
199
+
200
+	/**
201
+	 * Get all forums that the current user has special access to ("Extra forums" in the profile)
202
+	 * @return array Array of ForumIDs
203
+	 */
204
+	public static function get_permitted_forums() {
205
+		if (isset(G::$LoggedUser['CustomForums'])) {
206
+			return (array)array_keys(G::$LoggedUser['CustomForums'], 1);
207
+		} else {
208
+			return array();
209
+		}
210
+	}
211
+
212
+	/**
213
+	 * Get all forums that the current user does not have access to ("Restricted forums" in the profile)
214
+	 * @return array Array of ForumIDs
215
+	 */
216
+	public static function get_restricted_forums() {
217
+	  if (isset(G::$LoggedUser['CustomForums'])) {
218
+			return (array)array_keys(G::$LoggedUser['CustomForums'], 0);
219
+		} else {
220
+			return array();
221
+		}
222
+	}
223
+
224
+	/**
225
+	 * Get the last read posts for the current user
226
+	 * @param array $Forums Array of forums as returned by self::get_forums()
227
+	 * @return array TopicID => array(TopicID, PostID, Page) where PostID is the ID of the last read post and Page is the page on which that post is
228
+	 */
229
+	public static function get_last_read($Forums) {
230
+		if (isset(G::$LoggedUser['PostsPerPage'])) {
231
+			$PerPage = G::$LoggedUser['PostsPerPage'];
232
+		} else {
233
+			$PerPage = POSTS_PER_PAGE;
234
+		}
235
+		$TopicIDs = array();
236
+		foreach ($Forums as $Forum) {
237
+			if (!empty($Forum['LastPostTopicID'])) {
238
+				$TopicIDs[] = $Forum['LastPostTopicID'];
239
+			}
240
+		}
241
+		if (!empty($TopicIDs)) {
242
+			$QueryID = G::$DB->get_query_id();
243
+			G::$DB->query("
244
+				SELECT
245
+					l.TopicID,
246
+					l.PostID,
247
+					CEIL(
248
+						(
249
+							SELECT
250
+								COUNT(p.ID)
251
+							FROM forums_posts AS p
252
+							WHERE p.TopicID = l.TopicID
253
+								AND p.ID <= l.PostID
254
+						) / $PerPage
255
+					) AS Page
256
+				FROM forums_last_read_topics AS l
257
+				WHERE l.TopicID IN(" . implode(',', $TopicIDs) . ") AND
258
+					l.UserID = '" . G::$LoggedUser['ID'] . "'");
259
+			$LastRead = G::$DB->to_array('TopicID', MYSQLI_ASSOC);
260
+			G::$DB->set_query_id($QueryID);
261
+		} else {
262
+			$LastRead = array();
263
+		}
264
+		return $LastRead;
265
+	}
266
+
267
+	/**
268
+	 * Add a note to a topic.
269
+	 * @param int $TopicID
270
+	 * @param string $Note
271
+	 * @param int|null $UserID
272
+	 * @return boolean
273
+	 */
274
+	public static function add_topic_note($TopicID, $Note, $UserID = null) {
275
+		if ($UserID === null) {
276
+			$UserID = G::$LoggedUser['ID'];
277
+		}
278
+		$QueryID = G::$DB->get_query_id();
279
+		G::$DB->query("
280
+			INSERT INTO forums_topic_notes
281
+				(TopicID, AuthorID, AddedTime, Body)
282
+			VALUES
283
+				($TopicID, $UserID, '" . sqltime() . "', '" . db_string($Note) . "')");
284
+		G::$DB->set_query_id($QueryID);
285
+		return (bool)G::$DB->affected_rows();
286
+	}
287
+
288
+	/**
289
+	 * Determine if a thread is unread
290
+	 * @param bool $Locked
291
+	 * @param bool $Sticky
292
+	 * @param int $LastPostID
293
+	 * @param array $LastRead An array as returned by self::get_last_read
294
+	 * @param int $LastTopicID TopicID of the thread where the most recent post was made
295
+	 * @param string $LastTime Datetime of the last post
296
+	 * @return bool
297
+	 */
298
+	public static function is_unread($Locked, $Sticky, $LastPostID, $LastRead, $LastTopicID, $LastTime) {
299
+		return (!$Locked || $Sticky) && $LastPostID != 0 && ((empty($LastRead[$LastTopicID]) || $LastRead[$LastTopicID]['PostID'] < $LastPostID) && strtotime($LastTime) > G::$LoggedUser['CatchupTime']);
300
+	}
301
+
302
+	/**
303
+	 * Create the part of WHERE in the sql queries used to filter forums for a
304
+	 * specific user (MinClassRead, restricted and permitted forums).
305
+	 * @return string
306
+	 */
307
+	public static function user_forums_sql() {
308
+		// I couldn't come up with a good name, please rename this if you can. -- Y
309
+		$RestrictedForums = self::get_restricted_forums();
310
+		$PermittedForums = self::get_permitted_forums();
311
+		if (Donations::has_donor_forum(G::$LoggedUser['ID']) && !in_array(DONOR_FORUM, $PermittedForums)) {
312
+			$PermittedForums[] = DONOR_FORUM;
313
+		}
314
+		$SQL = "((f.MinClassRead <= '" . G::$LoggedUser['Class'] . "'";
315
+		if (count($RestrictedForums)) {
316
+			$SQL .= " AND f.ID NOT IN ('" . implode("', '", $RestrictedForums) . "')";
317
+		}
318
+		$SQL .= ')';
319
+		if (count($PermittedForums)) {
320
+			$SQL .= " OR f.ID IN ('" . implode("', '", $PermittedForums) . "')";
321
+		}
322
+		$SQL .= ')';
323
+		return $SQL;
324
+	}
325
+}

+ 13
- 0
classes/g.class.php View File

1
+<?
2
+class G {
3
+	public static $DB;
4
+	public static $Cache;
5
+	public static $LoggedUser;
6
+
7
+	public static function initialize() {
8
+		global $DB, $Cache, $LoggedUser;
9
+		self::$DB = $DB;
10
+		self::$Cache = $Cache;
11
+		self::$LoggedUser =& $LoggedUser;
12
+	}
13
+}

+ 58
- 0
classes/image.class.php View File

1
+<?
2
+if (!extension_loaded('gd')) {
3
+	error('GD Extension not loaded.');
4
+}
5
+
6
+class IMAGE {
7
+	var $Image = false;
8
+	var $FontSize = 10;
9
+	var $Font = '';
10
+	var $TextAngle = 0;
11
+
12
+	function create($Width, $Height) {
13
+		$this->Image = imagecreate($Width, $Height);
14
+		$this->Font = SERVER_ROOT.'/classes/fonts/VERDANA.TTF';
15
+		if (function_exists('imageantialias')) {
16
+			imageantialias($this->Image, true);
17
+		}
18
+	}
19
+
20
+	function color($Red, $Green, $Blue, $Alpha = 0) {
21
+		return imagecolorallocatealpha($this->Image, $Red, $Green, $Blue, $Alpha);
22
+	}
23
+
24
+	function line($x1, $y1, $x2, $y2, $Color, $Thickness = 1) {
25
+		if ($Thickness == 1) {
26
+			return imageline($this->Image, $x1, $y1, $x2, $y2, $Color);
27
+		}
28
+		$t = $Thickness / 2 - 0.5;
29
+		if ($x1 == $x2 || $y1 == $y2) {
30
+			return imagefilledrectangle($this->Image, round(min($x1, $x2) - $t), round(min($y1, $y2) - $t), round(max($x1, $x2) + $t), round(max($y1, $y2) + $t), $color);
31
+		}
32
+		$k = ($y2 - $y1) / ($x2 - $x1); //y = kx + q
33
+		$a = $t / sqrt(1 + pow($k, 2));
34
+		$Points = array(
35
+			round($x1 - (1 + $k) * $a), round($y1 + (1 - $k) * $a),
36
+			round($x1 - (1 - $k) * $a), round($y1 - (1 + $k) * $a),
37
+			round($x2 + (1 + $k) * $a), round($y2 - (1 - $k) * $a),
38
+			round($x2 + (1 - $k) * $a), round($y2 + (1 + $k) * $a),
39
+		);
40
+		imagefilledpolygon($this->Image, $Points, 4, $Color);
41
+		return imagepolygon($this->Image, $Points, 4, $Color);
42
+	}
43
+
44
+	function ellipse($x, $y, $Width, $Height, $Color) {
45
+		return imageEllipse($this->Image, $x, $y, $Width, $Height, $Color);
46
+	}
47
+
48
+	function text($x, $y, $Color, $Text) {
49
+		return imagettftext ($this->Image, $this->FontSize, $this->TextAngle, $x, $y, $Color, $this->Font, $Text);
50
+	}
51
+
52
+	function make_png($FileName = null) {
53
+		return imagepng($this->Image, $FileName);
54
+	}
55
+
56
+
57
+}
58
+?>

+ 251
- 0
classes/imagetools.class.php View File

1
+<?php
2
+
3
+/**
4
+ * ImageTools Class
5
+ * Thumbnail aide, mostly
6
+ */
7
+class ImageTools {
8
+	/**
9
+	 * Store processed links to avoid repetition
10
+	 * @var array 'URL' => 'Parsed URL'
11
+	 */
12
+	private static $Storage = array();
13
+
14
+	/**
15
+	 * We use true as an extra property to make the domain an array key
16
+	 * @var array $Hosts Array of image hosts
17
+	 */
18
+	private static $Hosts = array(
19
+//		'imgur.com' => true,
20
+		'whatimg.com' => true
21
+	);
22
+
23
+	/**
24
+	 * Blacklisted sites
25
+	 * @var array $Blacklist Array of blacklisted hosts
26
+	 */
27
+	private static $Blacklist = array(
28
+		'tinypic.com'
29
+	);
30
+
31
+	/**
32
+	 * Array of image hosts that provide thumbnailing
33
+	 * @var array $Thumbs
34
+	 */
35
+	private static $Thumbs = array(
36
+//		'i.imgur.com' => true,
37
+		'whatimg.com' => true
38
+	);
39
+
40
+	/**
41
+	 * Array of extensions
42
+	 * @var array $Extensions
43
+	 */
44
+	private static $Extensions = array(
45
+		'jpg' => true,
46
+		'jpeg' => true,
47
+		'png' => true,
48
+		'gif' => true,
49
+	);
50
+
51
+	/**
52
+	 * Array of user IDs whose avatars have been checked for size
53
+	 * @var array $CheckedAvatars
54
+	 */
55
+	private static $CheckedAvatars = array();
56
+	private static $CheckedAvatars2 = array();
57
+
58
+	/**
59
+	 * Array of user IDs whose donor icons have been checked for size
60
+	 * @var array $CheckedDonorIcons
61
+	 */
62
+	private static $CheckedDonorIcons = array();
63
+
64
+	/**
65
+	 * Checks from our list of valid hosts
66
+	 * @param string $Host Domain/host to check
67
+	 * @return boolean
68
+	 */
69
+	public static function valid_host($Host) {
70
+		return !empty(self::$Hosts[$Host]) && self::$Hosts[$Host] === true;
71
+	}
72
+
73
+	/**
74
+	 * Checks if a link's host is (not) good, otherwise displays an error.
75
+	 * @param string $Url Link to an image
76
+	 * @return boolean
77
+	 */
78
+	public static function blacklisted($Url, $ShowError = true) {
79
+		foreach (self::$Blacklist as &$Value) {
80
+			$Blacklisted = stripos($Url, $Value);
81
+			if ($Blacklisted !== false) {
82
+				$ParsedUrl = parse_url($Url);
83
+				if ($ShowError) {
84
+					error($ParsedUrl['host'] . ' is not an allowed image host. Please use a different host.');
85
+				}
86
+				return true;
87
+			}
88
+		}
89
+		return false;
90
+	}
91
+
92
+	/**
93
+	 * Checks to see if a link has a thumbnail
94
+	 * @param string $Url Link to an image
95
+	 * @return string|false Matched host or false
96
+	 */
97
+	private static function thumbnailable($Url) {
98
+		$ParsedUrl = parse_url($Url);
99
+		return !empty(self::$Thumbs[$ParsedUrl['host']]);
100
+	}
101
+
102
+	/**
103
+	 * Checks an extension
104
+	 * @param string $Ext Extension to check
105
+	 * @return boolean
106
+	 */
107
+	private static function valid_extension($Ext) {
108
+//		return @self::$Extensions[$Ext] === true;
109
+		return !empty(self::$Extensions[$Ext]) && (self::$Extensions[$Ext] === true);
110
+	}
111
+
112
+	/**
113
+	 * Stores a link with a (thumbnail) link
114
+	 * @param type $Link
115
+	 * @param type $Processed
116
+	 */
117
+	private static function store($Link, $Processed) {
118
+		self::$Storage[$Link] = $Processed;
119
+	}
120
+
121
+	/**
122
+	 * Retrieves an entry from our storage
123
+	 * @param type $Link
124
+	 * @return boolean|string Returns false if no match
125
+	 */
126
+	private static function get_stored($Link) {
127
+		if (isset(self::$Storage[$Link])) {
128
+			return self::$Storage[$Link];
129
+		}
130
+		return false;
131
+	}
132
+
133
+	/**
134
+	 * Checks if URL points to a whatimg thumbnail.
135
+	 */
136
+	private static function has_whatimg_thumb($Url) {
137
+		return (strpos($Url, '_thumb') !== false);
138
+	}
139
+
140
+	/**
141
+	 * Cleans up imgur URL if it already has a modifier attached to the end of it.
142
+	 */
143
+	private static function clean_imgur_url($Url) {
144
+		$Extension = pathinfo($Url, PATHINFO_EXTENSION);
145
+		$Full = preg_replace('/\.[^.]*$/', '', $Url);
146
+		$Base = substr($Full, 0, strrpos($Full, '/'));
147
+		$Path = substr($Full, strrpos($Full, '/') + 1);
148
+		if (strlen($Path) == 6) {
149
+			$Last = $Path[strlen($Path) - 1];
150
+			if ($Last == 'm' || $Last == 'l' || $Last == 's' || $Last == 'h' || $Last == 'b') {
151
+				$Path = substr($Path, 0, -1);
152
+			}
153
+		}
154
+		return "$Base/$Path.$Extension";
155
+	}
156
+
157
+	/**
158
+	 * Replaces the extension.
159
+	 */
160
+	private static function replace_extension($String, $Extension) {
161
+		return preg_replace('/\.[^.]*$/', $Extension, $String);
162
+	}
163
+
164
+	/**
165
+	 * Create image proxy URL
166
+	 * @param string $Url image URL
167
+	 * @param bool/string $CheckSize - accepts one of false, "avatar", "avatar2", or "donoricon"
168
+	 * @param bool/string/number $UserID - user ID for avatars and donor icons
169
+	 * @return image proxy URL
170
+	 */
171
+	public static function proxy_url($Url, $CheckSize, $UserID, &$ExtraInfo) {
172
+		global $SSL;
173
+
174
+		if ($UserID) {
175
+			$ExtraInfo = "&amp;userid=$UserID";
176
+			if ($CheckSize === 'avatar' && !isset(self::$CheckedAvatars[$UserID])) {
177
+				$ExtraInfo .= "&amp;type=$CheckSize";
178
+				self::$CheckedAvatars[$UserID] = true;
179
+			} elseif ($CheckSize === 'avatar2' && !isset(self::$CheckedAvatars2[$UserID])) {
180
+				$ExtraInfo .= "&amp;type=$CheckSize";
181
+				self::$CheckedAvatars2[$UserID] = true;
182
+			} elseif ($CheckSize === 'donoricon' && !isset(self::$CheckedDonorIcons[$UserID])) {
183
+				$ExtraInfo .= "&amp;type=$CheckSize";
184
+				self::$CheckedDonorIcons[$UserID] = true;
185
+			}
186
+		}
187
+
188
+    if (preg_match('/^https:\/\/'.SITE_URL.'\//', $Url)) {
189
+      return $Url;
190
+    } else {
191
+      return ($SSL ? 'https' : 'http') . '://' . SITE_URL . "/image.php?c=1&amp;i=" . urlencode($Url);
192
+    }
193
+	}
194
+
195
+	/**
196
+	 * Determine the image URL. This takes care of the image proxy and thumbnailing.
197
+	 * @param string $Url
198
+	 * @param bool $Thumb
199
+	 * @param bool/string $CheckSize - accepts one of false, "avatar", "avatar2", or "donoricon"
200
+	 * @param bool/string/number $UserID - user ID for avatars and donor icons
201
+	 * @return string
202
+	 */
203
+	public static function process($Url, $Thumb = false, $CheckSize = false, $UserID = false) {
204
+		if (empty($Url)) {
205
+			return '';
206
+		}
207
+
208
+		if ($Found = self::get_stored($Url . ($Thumb ? '_thumb' : ''))) {
209
+			return $Found;
210
+		}
211
+
212
+		$ProcessedUrl = $Url;
213
+		if ($Thumb) {
214
+			$Extension = pathinfo($Url, PATHINFO_EXTENSION);
215
+			if (self::thumbnailable($Url) && self::valid_extension($Extension)) {
216
+				if (strpos($Url, 'whatimg') !== false && !self::has_whatimg_thumb($Url)) {
217
+					$ProcessedUrl = self::replace_extension($Url, '_thumb.' . $Extension);
218
+				} elseif (strpos($Url, 'imgur') !== false) {
219
+					$ProcessedUrl = self::replace_extension(self::clean_imgur_url($Url), 'm.' . $Extension);
220
+				}
221
+			}
222
+		}
223
+
224
+		$ExtraInfo = '';
225
+		if (check_perms('site_proxy_images')) {
226
+			$ProcessedUrl = self::proxy_url($ProcessedUrl, $CheckSize, $UserID, $ExtraInfo);
227
+		}
228
+		self::store($Url . ($Thumb ? '_thumb' : ''), $ProcessedUrl);
229
+		return $ProcessedUrl . $ExtraInfo;
230
+	}
231
+
232
+	/**
233
+	 * Cover art thumbnail in browse, on artist pages etc.
234
+	 * @global array $CategoryIcons
235
+	 * @param string $Url
236
+	 * @param int $CategoryID
237
+	 */
238
+	public static function cover_thumb($Url, $CategoryID) {
239
+		global $CategoryIcons;
240
+		if ($Url) {
241
+			$Src = self::process($Url, true);
242
+			$Lightbox = self::process($Url);
243
+		} else {
244
+			$Src = STATIC_SERVER . 'common/noartwork/' . $CategoryIcons[$CategoryID - 1];
245
+			$Lightbox = $Src;
246
+		}
247
+?>
248
+		<img src="<?=$Src?>" width="90" height="90" alt="Cover" onclick="lightbox.init('<?=$Lightbox?>', 90)" />
249
+<?
250
+	}
251
+}

+ 31
- 0
classes/inbox.class.php View File

1
+<?php
2
+
3
+class Inbox {
4
+	/*
5
+	 * Get the link to a user's inbox.
6
+	 * This is what handles the ListUnreadPMsFirst setting
7
+	 *
8
+	 * @param string - whether the inbox or sentbox should be loaded
9
+	 * @return string - the URL to a user's inbox
10
+	 */
11
+	public static function get_inbox_link($WhichBox = 'inbox') {
12
+
13
+		$ListFirst = isset(G::$LoggedUser['ListUnreadPMsFirst']) ? G::$LoggedUser['ListUnreadPMsFirst'] : false;
14
+
15
+		if ($WhichBox == 'inbox') {
16
+			if ($ListFirst) {
17
+				$InboxURL = 'inbox.php?sort=unread';
18
+			} else {
19
+				$InboxURL = 'inbox.php';
20
+			}
21
+		} else {
22
+			if ($ListFirst) {
23
+				$InboxURL = 'inbox.php?action=sentbox&amp;sort=unread';
24
+			} else {
25
+				$InboxURL = 'inbox.php?action=sentbox';
26
+			}
27
+		}
28
+		return $InboxURL;
29
+	}
30
+}
31
+?>

+ 245
- 0
classes/invite_tree.class.php View File

1
+<?
2
+/**************************************************************************/
3
+/*-- Invite tree class -----------------------------------------------------
4
+
5
+
6
+
7
+***************************************************************************/
8
+
9
+class INVITE_TREE {
10
+	var $UserID = 0;
11
+	var $Visible = true;
12
+
13
+	// Set things up
14
+	function INVITE_TREE($UserID, $Options = array()) {
15
+		$this->UserID = $UserID;
16
+		if ($Options['visible'] === false) {
17
+			$this->Visible = false;
18
+		}
19
+	}
20
+
21
+	function make_tree() {
22
+		$QueryID = G::$DB->get_query_id();
23
+
24
+		$UserID = $this->UserID;
25
+?>
26
+		<div class="invitetree pad">
27
+<?
28
+		G::$DB->query("
29
+			SELECT TreePosition, TreeID, TreeLevel
30
+			FROM invite_tree
31
+			WHERE UserID = $UserID");
32
+		list($TreePosition, $TreeID, $TreeLevel) = G::$DB->next_record(MYSQLI_NUM, false);
33
+
34
+		if (!$TreeID) {
35
+			return;
36
+		}
37
+		G::$DB->query("
38
+			SELECT TreePosition
39
+			FROM invite_tree
40
+			WHERE TreeID = $TreeID
41
+				AND TreeLevel = $TreeLevel
42
+				AND TreePosition > $TreePosition
43
+			ORDER BY TreePosition ASC
44
+			LIMIT 1");
45
+		if (G::$DB->has_results()) {
46
+			list($MaxPosition) = G::$DB->next_record(MYSQLI_NUM, false);
47
+		} else {
48
+			$MaxPosition = false;
49
+		}
50
+		$TreeQuery = G::$DB->query("
51
+			SELECT
52
+				it.UserID,
53
+				Enabled,
54
+				PermissionID,
55
+				Donor,
56
+				Uploaded,
57
+				Downloaded,
58
+				Paranoia,
59
+				TreePosition,
60
+				TreeLevel
61
+			FROM invite_tree AS it
62
+				JOIN users_main AS um ON um.ID = it.UserID
63
+				JOIN users_info AS ui ON ui.UserID = it.UserID
64
+			WHERE TreeID = $TreeID
65
+				AND TreePosition > $TreePosition".
66
+				($MaxPosition ? " AND TreePosition < $MaxPosition" : '')."
67
+				AND TreeLevel > $TreeLevel
68
+			ORDER BY TreePosition");
69
+
70
+		$PreviousTreeLevel = $TreeLevel;
71
+
72
+		// Stats for the summary
73
+		$MaxTreeLevel = $TreeLevel; // The deepest level (this changes)
74
+		$OriginalTreeLevel = $TreeLevel; // The level of the user we're viewing
75
+		$BaseTreeLevel = $TreeLevel + 1; // The level of users invited by our user
76
+		$Count = 0;
77
+		$Branches = 0;
78
+		$DisabledCount = 0;
79
+		$DonorCount = 0;
80
+		$ParanoidCount = 0;
81
+		$TotalUpload = 0;
82
+		$TotalDownload = 0;
83
+		$TopLevelUpload = 0;
84
+		$TopLevelDownload = 0;
85
+
86
+		$ClassSummary = array();
87
+		global $Classes;
88
+		foreach ($Classes as $ClassID => $Val) {
89
+			$ClassSummary[$ClassID] = 0;
90
+		}
91
+
92
+		// We store this in an output buffer, so we can show the summary at the top without having to loop through twice
93
+		ob_start();
94
+		while (list($ID, $Enabled, $Class, $Donor, $Uploaded, $Downloaded, $Paranoia, $TreePosition, $TreeLevel) = G::$DB->next_record(MYSQLI_NUM, false)) {
95
+
96
+			// Do stats
97
+			$Count++;
98
+
99
+			if ($TreeLevel > $MaxTreeLevel) {
100
+				$MaxTreeLevel = $TreeLevel;
101
+			}
102
+
103
+			if ($TreeLevel == $BaseTreeLevel) {
104
+				$Branches++;
105
+				$TopLevelUpload += $Uploaded;
106
+				$TopLevelDownload += $Downloaded;
107
+			}
108
+
109
+			$ClassSummary[$Class]++;
110
+			if ($Enabled == 2) {
111
+				$DisabledCount++;
112
+			}
113
+			if ($Donor) {
114
+				$DonorCount++;
115
+			}
116
+
117
+			// Manage tree depth
118
+			if ($TreeLevel > $PreviousTreeLevel) {
119
+				for ($i = 0; $i < $TreeLevel - $PreviousTreeLevel; $i++) {
120
+					echo "\n\n<ul class=\"invitetree\">\n\t<li>\n";
121
+				}
122
+			} elseif ($TreeLevel < $PreviousTreeLevel) {
123
+				for ($i = 0; $i < $PreviousTreeLevel - $TreeLevel; $i++) {
124
+					echo "\t</li>\n</ul>\n";
125
+				}
126
+				echo "\t</li>\n\t<li>\n";
127
+			} else {
128
+				echo "\t</li>\n\t<li>\n";
129
+			}
130
+			$UserClass = $Classes[$Class]['Level'];
131
+?>
132
+		<strong><?=Users::format_username($ID, true, true, ($Enabled != 2 ? false : true), true)?></strong>
133
+<?
134
+			if (check_paranoia(array('uploaded', 'downloaded'), $Paranoia, $UserClass)) {
135
+				$TotalUpload += $Uploaded;
136
+				$TotalDownload += $Downloaded;
137
+?>
138
+		&nbsp;Uploaded: <strong><?=Format::get_size($Uploaded)?></strong>
139
+		&nbsp;Downloaded: <strong><?=Format::get_size($Downloaded)?></strong>
140
+		&nbsp;Ratio: <strong><?=Format::get_ratio_html($Uploaded, $Downloaded)?></strong>
141
+<?
142
+			} else {
143
+				$ParanoidCount++;
144
+?>
145
+		&nbsp;Hidden
146
+<?
147
+			}
148
+?>
149
+
150
+<?
151
+			$PreviousTreeLevel = $TreeLevel;
152
+			G::$DB->set_query_id($TreeQuery);
153
+		}
154
+
155
+		$Tree = ob_get_clean();
156
+		for ($i = 0; $i < $PreviousTreeLevel - $OriginalTreeLevel; $i++) {
157
+			$Tree .= "\t</li>\n</ul>\n";
158
+		}
159
+
160
+		if ($Count) {
161
+
162
+?>
163
+		<p style="font-weight: bold;">
164
+			This tree has <?=number_format($Count)?> entries, <?=number_format($Branches)?> branches, and a depth of <?=number_format($MaxTreeLevel - $OriginalTreeLevel)?>.
165
+			It has
166
+<?
167
+			$ClassStrings = array();
168
+			foreach ($ClassSummary as $ClassID => $ClassCount) {
169
+				if ($ClassCount == 0) {
170
+					continue;
171
+				}
172
+				$LastClass = Users::make_class_string($ClassID);
173
+				if ($ClassCount > 1) {
174
+					if ($LastClass == 'Torrent Celebrity') {
175
+						 $LastClass = 'Torrent Celebrities';
176
+					} else {
177
+						$LastClass.='s';
178
+					}
179
+				}
180
+				$LastClass = "$ClassCount $LastClass (" . number_format(($ClassCount / $Count) * 100) . '%)';
181
+
182
+				$ClassStrings[] = $LastClass;
183
+			}
184
+			if (count($ClassStrings) > 1) {
185
+				array_pop($ClassStrings);
186
+				echo implode(', ', $ClassStrings);
187
+				echo ' and '.$LastClass;
188
+			} else {
189
+				echo $LastClass;
190
+			}
191
+			echo '. ';
192
+			echo $DisabledCount;
193
+			echo ($DisabledCount == 1) ? ' user is' : ' users are';
194
+			echo ' disabled (';
195
+			if ($DisabledCount == 0) {
196
+				echo '0%)';
197
+			} else {
198
+				echo number_format(($DisabledCount / $Count) * 100) . '%)';
199
+			}
200
+			echo ', and ';
201
+			echo $DonorCount;
202
+			echo ($DonorCount == 1) ? ' user has' : ' users have';
203
+			echo ' donated (';
204
+			if ($DonorCount == 0) {
205
+				echo '0%)';
206
+			} else {
207
+				echo number_format(($DonorCount / $Count) * 100) . '%)';
208
+			}
209
+			echo '. </p>';
210
+
211
+			echo '<p style="font-weight: bold;">';
212
+			echo 'The total amount uploaded by the entire tree was '.Format::get_size($TotalUpload);
213
+			echo '; the total amount downloaded was '.Format::get_size($TotalDownload);
214
+			echo '; and the total ratio is '.Format::get_ratio_html($TotalUpload, $TotalDownload).'. ';
215
+			echo '</p>';
216
+
217
+			echo '<p style="font-weight: bold;">';
218
+			echo 'The total amount uploaded by direct invitees (the top level) was '.Format::get_size($TopLevelUpload);
219
+			echo '; the total amount downloaded was '.Format::get_size($TopLevelDownload);
220
+			echo '; and the total ratio is '.Format::get_ratio_html($TopLevelUpload, $TopLevelDownload).'. ';
221
+
222
+			echo "These numbers include the stats of paranoid users and will be factored into the invitation giving script.\n\t\t</p>\n";
223
+
224
+			if ($ParanoidCount) {
225
+				echo '<p style="font-weight: bold;">';
226
+				echo $ParanoidCount;
227
+				echo ($ParanoidCount == 1) ? ' user (' : ' users (';
228
+				echo number_format(($ParanoidCount / $Count) * 100);
229
+				echo '%) ';
230
+				echo ($ParanoidCount == 1) ? ' is' : ' are';
231
+				echo ' too paranoid to have their stats shown here, and ';
232
+				echo ($ParanoidCount == 1) ? ' was' : ' were';
233
+				echo ' not factored into the stats for the total tree.';
234
+				echo '</p>';
235
+			}
236
+		}
237
+?>
238
+			<br />
239
+<?=			$Tree?>
240
+		</div>
241
+<?
242
+		G::$DB->set_query_id($QueryID);
243
+	}
244
+}
245
+?>

+ 183
- 0
classes/irc.class.php View File

1
+<?
2
+class IRC_DB extends DB_MYSQL {
3
+	function halt($Msg) {
4
+		global $Bot;
5
+		$Bot->send_to($Bot->get_channel(), 'The database is currently unavailable; try again later.');
6
+	}
7
+}
8
+
9
+abstract class IRC_BOT {
10
+	abstract protected function connect_events();
11
+	abstract protected function channel_events();
12
+	abstract protected function query_events();
13
+	abstract protected function irc_events();
14
+	abstract protected function listener_events();
15
+
16
+	protected $Debug = false;
17
+	protected $Socket = false;
18
+	protected $Data = false;
19
+	protected $Whois = false;
20
+	protected $Identified = array();
21
+	protected $Channels = array();
22
+	protected $Messages = array();
23
+	protected $LastChan = false;
24
+	protected $ListenSocket = false;
25
+	protected $Listened = false;
26
+	protected $Connecting = false;
27
+	protected $State = 1; // Drone is live
28
+	public $Restart = 0; // Die by default
29
+
30
+	public function __construct() {
31
+		if (isset($_SERVER['HOME']) && is_dir($_SERVER['HOME']) && getcwd() != $_SERVER['HOME']) {
32
+			chdir($_SERVER['HOME']);
33
+		}
34
+		ob_end_clean();
35
+		restore_error_handler(); //Avoid PHP error logging
36
+		set_time_limit(0);
37
+	}
38
+
39
+	public function connect() {
40
+		$this->connect_irc();
41
+		$this->connect_listener();
42
+		$this->post_connect();
43
+	}
44
+
45
+	private function connect_irc($Reconnect = false) {
46
+		$this->Connecting = true;
47
+		//Open a socket to the IRC server
48
+		if (defined('BOT_PORT_SSL')) {
49
+			$IrcAddress = 'tls://' . BOT_SERVER . ':' . BOT_PORT_SSL;
50
+		} else {
51
+			$IrcAddress = 'tcp://' . BOT_SERVER . ':' . BOT_PORT;
52
+		}
53
+		while (!$this->Socket = stream_socket_client($IrcAddress, $ErrNr, $ErrStr)) {
54
+			sleep(15);
55
+		}
56
+		stream_set_blocking($this->Socket, 0);
57
+		$this->Connecting = false;
58
+		if ($Reconnect) {
59
+			$this->post_connect();
60
+		}
61
+	}
62
+
63
+	private function connect_listener() {
64
+		//create a socket to listen on
65
+		$ListenAddress = 'tcp://' . SOCKET_LISTEN_ADDRESS . ':' . SOCKET_LISTEN_PORT;
66
+		if (!$this->ListenSocket = stream_socket_server($ListenAddress, $ErrNr, $ErrStr)) {
67
+			die("Cannot create listen socket: $ErrStr");
68
+		}
69
+		stream_set_blocking($this->ListenSocket, false);
70
+	}
71
+
72
+	private function post_connect() {
73
+		fwrite($this->Socket, "NICK ".BOT_NICK."Init\n");
74
+		fwrite($this->Socket, "USER ".BOT_NICK." * * :IRC Bot\n");
75
+		$this->listen();
76
+	}
77
+
78
+	public function disconnect() {
79
+		fclose($this->ListenSocket);
80
+		$this->State = 0; //Drones dead
81
+	}
82
+
83
+	public function get_channel() {
84
+		preg_match('/.+ PRIVMSG ([^:]+) :.+/', $this->Data, $Channel);
85
+		if (preg_match('/#.+/', $Channel[1])) {
86
+			return $Channel[1];
87
+		} else {
88
+			return false;
89
+		}
90
+	}
91
+
92
+	public function get_nick() {
93
+		preg_match('/:([^!:]+)!.+@[^\s]+ PRIVMSG [^:]+ :.+/', $this->Data, $Nick);
94
+		return $Nick[1];
95
+	}
96
+
97
+	protected function get_message() {
98
+		preg_match('/:.+ PRIVMSG [^:]+ :(.+)/', $this->Data, $Msg);
99
+		return trim($Msg[1]);
100
+	}
101
+
102
+	protected function get_irc_host() {
103
+		preg_match('/:[^!:]+!.+@([^\s]+) PRIVMSG [^:]+ :.+/', $this->Data, $Host);
104
+		return trim($Host[1]);
105
+	}
106
+
107
+	protected function get_word($Select = 1) {
108
+		preg_match('/:.+ PRIVMSG [^:]+ :(.+)/', $this->Data, $Word);
109
+		$Word = split(' ', $Word[1]);
110
+		return trim($Word[$Select]);
111
+	}
112
+
113
+	protected function get_action() {
114
+		preg_match('/:.+ PRIVMSG [^:]+ :!(\S+)/', $this->Data, $Action);
115
+		return strtoupper($Action[1]);
116
+	}
117
+
118
+	protected function send_raw($Text) {
119
+		if (!feof($this->Socket)) {
120
+			fwrite($this->Socket, "$Text\n");
121
+		} elseif (!$this->Connecting) {
122
+			$this->Connecting = true;
123
+			sleep(120);
124
+			$this->connect_irc(true);
125
+		}
126
+	}
127
+
128
+	public function send_to($Channel, $Text) {
129
+		// split the message up into <= 460 character strings and send each individually
130
+		// this is used to prevent messages from getting truncated
131
+		$Text = wordwrap($Text, 460, "\n", true);
132
+		$TextArray = explode("\n", $Text);
133
+		foreach ($TextArray as $Text) {
134
+			$this->send_raw("PRIVMSG $Channel :$Text");
135
+		}
136
+	}
137
+
138
+	protected function whois($Nick) {
139
+		$this->Whois = $Nick;
140
+		$this->send_raw("WHOIS $Nick");
141
+	}
142
+
143
+	/*
144
+	This function uses blacklisted_ip, which is no longer in RC2.
145
+	You can probably find it in old RC1 code kicking aronud if you need it.
146
+	protected function ip_check($IP, $Gline = false, $Channel = BOT_REPORT_CHAN) {
147
+		if (blacklisted_ip($IP)) {
148
+			$this->send_to($Channel, 'TOR IP Detected: '.$IP);
149
+			if ($Gline) {
150
+				$this->send_raw('GLINE *@'.$IP.' 90d :DNSBL Proxy');
151
+			}
152
+		}
153
+		if (Tools::site_ban_ip($IP)) {
154
+			$this->send_to($Channel, 'Site IP Ban Detected: '.$IP);
155
+			if ($Gline) {
156
+				$this->send_raw('GLINE *@'.$IP.' 90d :IP Ban');
157
+			}
158
+		}
159
+	}*/
160
+
161
+	protected function listen() {
162
+		G::$Cache->InternalCache = false;
163
+		stream_set_timeout($this->Socket, 10000000000);
164
+		while ($this->State == 1) {
165
+			$NullSock = null;
166
+			$Sockets = array($this->Socket, $this->ListenSocket);
167
+			if (stream_select($Sockets, $NullSock, $NullSock, null) === false) {
168
+				die();
169
+			}
170
+			foreach ($Sockets as $Socket) {
171
+				if ($Socket === $this->Socket) {
172
+					$this->irc_events();
173
+				} else {
174
+					$this->Listened = stream_socket_accept($Socket);
175
+					$this->listener_events();
176
+				}
177
+			}
178
+			G::$DB->LinkID = false;
179
+			G::$DB->Queries = array();
180
+		}
181
+	}
182
+}
183
+?>

+ 60
- 0
classes/lockedaccounts.class.php View File

1
+<?
2
+
3
+/**
4
+ * Class to manage locked accounts
5
+ */
6
+class LockedAccounts
7
+{
8
+
9
+    /**
10
+     * Lock an account
11
+     *
12
+     * @param int $UserID The ID of the user to lock
13
+     * @param int $Type The lock type, should be a constant value
14
+     * @param string $Message The message to write to user notes
15
+     * @param string $Reason The reason for the lock
16
+     * @param int $LockedByUserID The ID of the staff member that locked $UserID's account. 0 for system
17
+     */
18
+    public static function lock_account($UserID, $Type, $Message, $Reason, $LockedByUserID)
19
+    {
20
+        
21
+        if ($LockedByUserID == 0) {
22
+            $Username = "System";
23
+        } else {
24
+            G::$DB->query("SELECT Username FROM users_main WHERE ID = '" . $LockedByUserID . "'");
25
+            list($Username) = G::$DB->next_record();
26
+        }
27
+
28
+        G::$DB->query("
29
+                INSERT INTO locked_accounts (UserID, Type)
30
+                VALUES ('" . $UserID . "', " . $Type . ")");
31
+        Tools::update_user_notes($UserID, sqltime() . " - " . db_string($Message) . " by $Username\nReason: " . db_string($Reason) . "\n\n");
32
+        G::$Cache->delete_value('user_info_' . $UserID);
33
+    }
34
+
35
+    /**
36
+     * Unlock an account
37
+     *
38
+     * @param int $UserID The ID of the user to unlock
39
+     * @param int $Type The lock type, should be a constant value. Used for database verification
40
+     *                  to avoid deleting the wrong lock type
41
+     * @param string $Reason The reason for unlock
42
+     * @param int $UnlockedByUserID The ID of the staff member unlocking $UserID's account. 0 for system
43
+     */
44
+    public static function unlock_account($UserID, $Type, $Message, $Reason, $UnlockedByUserID)
45
+    {
46
+        if ($UnlockedByUserID == 0) {
47
+            $Username = "System";
48
+        } else {
49
+            G::$DB->query("SELECT Username FROM users_main WHERE ID = '" . $UnlockedByUserID . "'");
50
+            list($Username) = G::$DB->next_record();
51
+        }
52
+
53
+        G::$DB->query("DELETE FROM locked_accounts WHERE UserID = '$UserID' AND Type = '". $Type ."'");
54
+
55
+        if (G::$DB->affected_rows() == 1) {
56
+            G::$Cache->delete_value("user_info_" . $UserID);
57
+            Tools::update_user_notes($UserID, sqltime() . " - " . db_string($Message) . " by $Username\nReason: " . db_string($Reason) . "\n\n");
58
+        }
59
+    }
60
+}

+ 81
- 0
classes/mass_user_bookmarks_editor.class.php View File

1
+<?php
2
+
3
+//require_once 'mass_user_torrents_editor.class.php';
4
+
5
+/**
6
+ * This class helps with mass-editing bookmarked torrents.
7
+ *
8
+ * It can later be used for other bookmark tables.
9
+ *
10
+ */
11
+class MASS_USER_BOOKMARKS_EDITOR extends MASS_USER_TORRENTS_EDITOR {
12
+	public function __construct($Table = 'bookmarks_torrents') {
13
+		$this->set_table($Table);
14
+	}
15
+
16
+	/**
17
+	 * Runs a SQL query and clears the Cache key
18
+	 *
19
+	 * G::$Cache->delete_value didn't always work, but setting the key to null, did. (?)
20
+	 *
21
+	 * @param string $sql
22
+	 */
23
+	protected function query_and_clear_cache($sql) {
24
+		$QueryID = G::$DB->get_query_id();
25
+		if (is_string($sql) && G::$DB->query($sql)) {
26
+			G::$Cache->delete_value('bookmarks_group_ids_' . G::$LoggedUser['ID']);
27
+		}
28
+		G::$DB->set_query_id($QueryID);
29
+	}
30
+
31
+	/**
32
+	 * Uses (checkboxes) $_POST['remove'] to delete entries.
33
+	 *
34
+	 * Uses an IN() to match multiple items in one query.
35
+	 */
36
+	public function mass_remove() {
37
+		$SQL = array();
38
+		foreach ($_POST['remove'] as $GroupID => $K) {
39
+			if (is_number($GroupID)) {
40
+				$SQL[] = sprintf('%d', $GroupID);
41
+			}
42
+		}
43
+
44
+		if (!empty($SQL)) {
45
+			$SQL = sprintf('
46
+					DELETE FROM %s
47
+					WHERE UserID = %d
48
+						AND GroupID IN (%s)',
49
+				$this->Table,
50
+				G::$LoggedUser['ID'],
51
+				implode(', ', $SQL)
52
+			);
53
+			$this->query_and_clear_cache($SQL);
54
+		}
55
+	}
56
+
57
+	/**
58
+	 * Uses $_POST['sort'] values to update the DB.
59
+	 */
60
+	public function mass_update() {
61
+		$SQL = array();
62
+		foreach ($_POST['sort'] as $GroupID => $Sort) {
63
+			if (is_number($Sort) && is_number($GroupID)) {
64
+				$SQL[] = sprintf('(%d, %d, %d)', $GroupID, $Sort, G::$LoggedUser['ID']);
65
+			}
66
+		}
67
+
68
+		if (!empty($SQL)) {
69
+			$SQL = sprintf('
70
+					INSERT INTO %s
71
+						(GroupID, Sort, UserID)
72
+					VALUES
73
+						%s
74
+					ON DUPLICATE KEY UPDATE
75
+						Sort = VALUES (Sort)',
76
+				$this->Table,
77
+				implode(', ', $SQL));
78
+			$this->query_and_clear_cache($SQL);
79
+		}
80
+	}
81
+}

+ 61
- 0
classes/mass_user_torrents_editor.class.php View File

1
+<?php
2
+
3
+/**
4
+ * Abstract class
5
+ * Mass User-Torrents Editor
6
+ *
7
+ * A class that deals with mass editing a user's torrents.
8
+ *
9
+ * This abstract class is used by sub-classes as a way to access the Cache/DB.
10
+ *
11
+ * It is intended to streamline the process of processing data sent by the
12
+ * MASS_USER_TORRENT_TABLE_VIEW class.
13
+ *
14
+ * It could also be used for other types like collages.
15
+ */
16
+abstract class MASS_USER_TORRENTS_EDITOR {
17
+	/**
18
+	 * The affected DB table
19
+	 * @var string $Table
20
+	 */
21
+	protected $Table;
22
+
23
+	/**
24
+	 * Set the Table
25
+	 * @param string $Table
26
+	 */
27
+	final public function set_table($Table) {
28
+		$this->Table = db_string($Table);
29
+	}
30
+
31
+	/**
32
+	 * Get the Table
33
+	 * @return string $Table
34
+	 */
35
+	final public function get_table() {
36
+		return $this->Table;
37
+	}
38
+
39
+	/**
40
+	 * The extending class must provide a method to send a query and clear the cache
41
+	 */
42
+	abstract protected function query_and_clear_cache($sql);
43
+
44
+	/**
45
+	 * A method to insert many rows into a single table
46
+	 * Not required in subsequent classes
47
+	 */
48
+	public function mass_add() {}
49
+
50
+	/**
51
+	 * A method to remove many rows from a table
52
+	 * The extending class must have a mass_remove method
53
+	 */
54
+	abstract public function mass_remove();
55
+
56
+	/**
57
+	 * A method to update many rows in a table
58
+	 * The extending class must have a mass_update method
59
+	 */
60
+	abstract public function mass_update();
61
+}

+ 266
- 0
classes/mass_user_torrents_table_view.class.php View File

1
+<?php
2
+
3
+/**
4
+ * This class outputs a table that can be used to sort torrents through a drag/drop
5
+ * interface, an automatic column sorter, or manual imput.
6
+ *
7
+ * It places checkboxes to delete items.
8
+ *
9
+ * (It creates a div#thin.)
10
+ *
11
+ * It can be used for Bookmarks, Collages, or anywhere where torrents are managed.
12
+ */
13
+class MASS_USER_TORRENTS_TABLE_VIEW {
14
+	/**
15
+	 * Used to set text the page heading (h2 tag)
16
+	 * @var string $Heading
17
+	 */
18
+	private $Heading = 'Manage Torrents';
19
+
20
+	/**
21
+	 * Sets the value of the input name="type"
22
+	 * Later to be used as $_POST['type'] in a form processor
23
+	 * @var string $EditType
24
+	 */
25
+	private $EditType;
26
+
27
+	/**
28
+	 * Flag for empty $TorrentList
29
+	 * @var bool $HasTorrentList
30
+	 */
31
+	private $HasTorrents;
32
+
33
+	/**
34
+	 * Internal reference to the TorrentList
35
+	 * @var array $TorrentList
36
+	 */
37
+	private $TorrentList;
38
+
39
+	/**
40
+	 * Ref. to $CollageDataList
41
+	 * @var array $CollageDataList
42
+	 */
43
+	private $CollageDataList;
44
+
45
+	/**
46
+	 * Counter for number of groups
47
+	 * @var in $NumGroups
48
+	 */
49
+	private $NumGroups = 0;
50
+
51
+	/**
52
+	 * When creating a new instance of this class, TorrentList and
53
+	 * CollageDataList must be passed. Additionally, a heading can be added.
54
+	 *
55
+	 * @param array $TorrentList
56
+	 * @param array $CollageDataList
57
+	 * @param string $EditType
58
+	 * @param string $Heading
59
+	 */
60
+	public function __construct (array &$TorrentList, array &$CollageDataList, $EditType, $Heading = null) {
61
+		$this->set_heading($Heading);
62
+		$this->set_edit_type($EditType);
63
+
64
+		$this->TorrentList = $TorrentList;
65
+		$this->CollageDataList = $CollageDataList;
66
+
67
+		$this->HasTorrents = !empty($TorrentList);
68
+		if (!$this->HasTorrents) {
69
+			$this->no_torrents();
70
+		}
71
+	}
72
+
73
+	private function no_torrents () {
74
+?>
75
+		<div class="thin">
76
+			<div class="header">
77
+				<h2>No torrents found.</h2>
78
+			</div>
79
+			<div class="box pad" align="center">
80
+				<p>Add some torrents and come back later.</p>
81
+			</div>
82
+		</div>
83
+<?
84
+	}
85
+
86
+	/**
87
+	 * Renders a complete page and table
88
+	 */
89
+	public function render_all () {
90
+		$this->header();
91
+		$this->body();
92
+		$this->footer();
93
+	}
94
+
95
+	/**
96
+	 * Renders a comptele page/table header: div#thin, h2, scripts, notes,
97
+	 * form, table, etc.
98
+	 */
99
+	public function header () {
100
+		if ($this->HasTorrents) {
101
+?>
102
+
103
+<div class="thin">
104
+	<div class="header">
105
+		<h2><?=display_str($this->Heading)?></h2>
106
+	</div>
107
+
108
+	<table width="100%" class="layout box">
109
+		<tr class="colhead"><td id="sorting_head">Sorting</td></tr>
110
+		<tr>
111
+			<td id="drag_drop_textnote">
112
+			<ul>
113
+				<li>Click on the headings to organize columns automatically.</li>
114
+				<li>Sort multiple columns simultaneously by holding down the shift key and clicking other column headers.</li>
115
+				<li>Click and drag any row to change its order.</li>
116
+				<li>Double-click on a row to check it.</li>
117
+			</ul>
118
+			</td>
119
+		</tr>
120
+	</table>
121
+
122
+	<form action="bookmarks.php" method="post" id="drag_drop_collage_form">
123
+
124
+<?			$this->buttons(); ?>
125
+
126
+		<table id="manage_collage_table" class="box">
127
+			<thead>
128
+				<tr class="colhead">
129
+					<th style="width: 7%;" data-sorter="false">Order</th>
130
+					<th style="width: 1%;"><span><abbr class="tooltip" title="Current order">#</abbr></span></th>
131
+					<th style="width: 1%;"><span>Year</span></th>
132
+					<th style="width: 15%;" data-sorter="ignoreArticles"><span>Artist</span></th>
133
+					<th data-sorter="ignoreArticles"><span>Torrent</span></th>
134
+					<th style="width: 5%;" data-sorter="relativeTime"><span>Bookmarked</span></th>
135
+					<th style="width: 1%;" id="check_all" data-sorter="false"><span>Remove</span></th>
136
+				</tr>
137
+			</thead>
138
+			<tbody>
139
+<?
140
+		}
141
+	}
142
+
143
+	/**
144
+	 * Closes header code
145
+	 */
146
+	public function footer () {
147
+		if ($this->HasTorrents) {
148
+?>
149
+
150
+			</tbody>
151
+		</table>
152
+
153
+<?			$this->buttons(); ?>
154
+
155
+		<div>
156
+			<input type="hidden" name="action" value="mass_edit" />
157
+			<input type="hidden" name="type" value="<?=display_str($this->EditType)?>" />
158
+			<input type="hidden" name="auth" value="<?=G::$LoggedUser['AuthKey']?>" />
159
+		</div>
160
+	</form>
161
+</div>
162
+
163
+<?
164
+		}
165
+	}
166
+
167
+	/**
168
+	 * Formats data for use in row
169
+	 *
170
+	 */
171
+	public function body () {
172
+		if ($this->HasTorrents)
173
+			foreach ($this->TorrentList as $GroupID => $Group) {
174
+				$Artists = array();
175
+
176
+				extract($Group);
177
+				extract($this->CollageDataList[$GroupID]);
178
+
179
+				$this->NumGroups++;
180
+
181
+				$DisplayName = self::display_name($ExtendedArtists, $Artists, $VanityHouse);
182
+				$TorrentLink = '<a href="torrents.php?id='.$GroupID.'" class="tooltip" title="View torrent">'.$Name.'</a>';
183
+				$Year = $Year > 0 ? $Year : '';
184
+				$DateAdded = date($Time);
185
+
186
+				$this->row($Sort, $GroupID, $Year, $DisplayName, $TorrentLink, $DateAdded);
187
+			}
188
+	}
189
+
190
+	/**
191
+	 * Outputs a single row
192
+	 *
193
+	 * @param string|int $Sort
194
+	 * @param string|int $GroupID
195
+	 * @param string|int $GroupYear
196
+	 * @param string $DisplayName
197
+	 * @param string $TorrentLink
198
+	 */
199
+	public function row ($Sort, $GroupID, $GroupYear, $DisplayName, $TorrentLink, $DateAdded) {
200
+?>
201
+
202
+					<tr class="drag row" id="li_<?=$GroupID?>">
203
+						<td>
204
+							<input class="sort_numbers" type="text" name="sort[<?=$GroupID?>]" value="<?=$Sort?>" id="sort_<?=$GroupID?>" size="4" />
205
+						</td>
206
+						<td><?=$this->NumGroups?></td>
207
+						<td><?=$GroupYear ? trim($GroupYear) : ' '?></td>
208
+						<td><?=$DisplayName ? trim($DisplayName) : ' '?></td>
209
+						<td><?=$TorrentLink ? trim($TorrentLink) : ' '?></td>
210
+						<td class="nobr tooltip" title="<?=$DateAdded?>"><?=$DateAdded ? time_diff($DateAdded) : ' '?></td>
211
+						<td class="center"><input type="checkbox" name="remove[<?=$GroupID?>]" value="" /></td>
212
+					</tr>
213
+<?
214
+	}
215
+
216
+	/**
217
+	 * Parses a simple display name
218
+	 *
219
+	 * @param array $ExtendedArtists
220
+	 * @param array $Artists
221
+	 * @param string $VanityHouse
222
+	 * @return string $DisplayName
223
+	 */
224
+	public static function display_name (array &$ExtendedArtists, array &$Artists, $VanityHouse) {
225
+		$DisplayName = '';
226
+		if (!empty($ExtendedArtists[1]) || !empty($ExtendedArtists[4])
227
+				|| !empty($ExtendedArtists[5]) || !empty($ExtendedArtists[6])) {
228
+			unset($ExtendedArtists[2], $ExtendedArtists[3]);
229
+			$DisplayName = Artists::display_artists($ExtendedArtists, true, false);
230
+		} elseif (count($Artists) > 0) {
231
+			$DisplayName = Artists::display_artists(array('1'=>$Artists), true, false);
232
+		}
233
+		if ($VanityHouse) {
234
+			$DisplayName .= ' [<abbr class="tooltip" title="This is a Vanity House release">VH</abbr>]';
235
+		}
236
+		return $DisplayName;
237
+	}
238
+
239
+	/**
240
+	 * Renders buttons used at the top and bottom of the table
241
+	 */
242
+	public function buttons () {
243
+?>
244
+		<div class="drag_drop_save">
245
+			<input type="submit" name="update" value="Update ranking" title="Save your rank" class="tooltip save_sortable_collage" />
246
+			<input type="submit" name="delete" value="Delete checked" title="Remove items" class="tooltip save_sortable_collage" />
247
+		</div>
248
+<?
249
+	}
250
+
251
+
252
+	/**
253
+	 * @param string $EditType
254
+	 */
255
+	public function set_edit_type ($EditType) {
256
+		$this->EditType = $EditType;
257
+	}
258
+
259
+	/**
260
+	 * Set's the current page's heading
261
+	 * @param string $Heading
262
+	 */
263
+	public function set_heading ($Heading) {
264
+		$this->Heading = $Heading;
265
+	}
266
+}

+ 680
- 0
classes/mediainfo.class.php View File

1
+<?php
2
+class MediaInfo {
3
+    public static function parse($string, $raw=false) {
4
+        $t = new ParseManager($string);
5
+        $t->parse();
6
+        return $raw ? $t->output_raw() : $t->output();
7
+    }
8
+}
9
+
10
+class ParseManager {
11
+    protected $lines;
12
+    protected $parsed_lines;
13
+    protected $index;
14
+    protected $parsers;
15
+    protected $output;
16
+    protected $available_parsers = array(
17
+        'general'=> 'GeneralSectionParser',
18
+        'video'=> 'VideoSectionParser',
19
+        'audio'=> 'AudioSectionParser',
20
+        'text'=> 'TextSectionParser',
21
+    );
22
+
23
+    const GENERIC_PARSER = 'generic_parser';
24
+    const MEDIAINFO_START = 'general';
25
+    public function __construct($string='') {
26
+        $this->index = 0;
27
+        $this->output = '';
28
+        $this->parsed_lines = array();
29
+        $this->set_string($string);
30
+        $p = new SectionParser();
31
+        $this->add_parser($p);
32
+    }
33
+
34
+    protected function set_string($string) {
35
+        $string = trim($string);
36
+        $string = static::strip_escaped_tags($string);
37
+        $lines = preg_split("/\r\n|\n|\r/", $string);
38
+        array_walk($lines,function(&$l) {
39
+            $l=trim($l);
40
+        });
41
+        $this->lines = $lines;
42
+    }
43
+
44
+    protected function add_parser($p,$name='') {
45
+        $p->set_lines($this->lines,$this->index);
46
+        if (!$name) $name = static::GENERIC_PARSER;
47
+        $this->parsers[$name][] = $p;
48
+    }
49
+
50
+    public function parse() {
51
+        // get the next section
52
+        while ($this->index < count($this->lines) &&
53
+          !($line = $this->parsers[static::GENERIC_PARSER][0]->parse_line()));
54
+        $section = SectionParser::section_name($line);
55
+        $this->index--; // go back to line we just read
56
+
57
+        // we can have multiple mediainfo files inside the block, so handle that case here
58
+        if ($section == self::MEDIAINFO_START && isset($this->parsers[$section])) {
59
+            $this->new_mediainfo();
60
+        }
61
+        if (isset($this->available_parsers[$section])){
62
+            $parser = new $this->available_parsers[$section];
63
+            $this->add_parser($parser,$section);
64
+            // parse section using the parser
65
+            while ($line = $parser->parse_line()) $this->parsed_lines[] = $line;
66
+
67
+            $this->parsed_lines[] = '';
68
+        }
69
+        else {
70
+            // skip until the next blank line or until the next general section
71
+            while ($line = $this->parsers[static::GENERIC_PARSER][0]->parse_line()){
72
+                $section = SectionParser::section_name($line);
73
+                if ($section == self::MEDIAINFO_START) {
74
+                    $this->index--; // go back to line we just read
75
+                    break;
76
+                }
77
+            }
78
+        }
79
+
80
+        // keep iterating until the last line
81
+        if ($this->index < count($this->lines)) {
82
+            $this->parse();
83
+        }
84
+    }
85
+
86
+    public function output($cummulative=true) {
87
+        $string = implode("<br />\n",$this->parsed_lines);
88
+        if (!isset($this->parsers['general'])) return $string;
89
+
90
+        $midiv_start = '<div class="spoilerContainer hideContainer">
91
+            <input type="button" class="spoilerButton" onclick="BBCode.spoiler(this);" value="Show '.
92
+            $this->parsers['general'][0]->filename.
93
+            '" /><blockquote class="spoiler hidden">';
94
+        $midiv_end = "</blockquote></div>";
95
+
96
+        $output = '<table class="mediainfo"><tbody><tr><td>';
97
+        $output .= $this->parsers['general'][0]->output();
98
+        if (isset($this->parsers['video'])){
99
+            $output .= '</td><td>';
100
+            $output .= $this->parsers['video'][0]->output();
101
+        }
102
+        if (isset($this->parsers['audio'])){
103
+            $output .= '</td><td>';
104
+            $output .= '<table><caption>Audio</caption><tbody>';
105
+            foreach($this->parsers['audio'] as $index => $ap) {
106
+                $output .= sprintf('<tr><td>#%d: &nbsp;</td><td>%s</td></tr>',intval($index+1),$ap->output());
107
+            }
108
+            $output .= '</tbody></table>';
109
+        }
110
+        if (isset($this->parsers['text'])){
111
+            // subtitles table will be printed below the previous table
112
+            $output .= '<br />';
113
+            $output .= '<table><caption>Subtitles</caption><tbody>';
114
+            foreach($this->parsers['text'] as $index => $tp) {
115
+                $output .= sprintf('<tr><td>#%d: &nbsp;</td><td>%s</td></tr>',intval($index+1),$tp->output());
116
+            }
117
+            $output .= '</tbody></table>';
118
+        }
119
+        $output .= '</td></tr></tbody></table><br />';
120
+        $output = $midiv_start . $string . $midiv_end . $output;
121
+        if ($cummulative) {
122
+            $output = $this->output . $output;
123
+        }
124
+        return  $output;
125
+    }
126
+
127
+    public function output_raw() {
128
+        $output = array();
129
+        $sections = ['general', 'video', 'audio', 'text'];
130
+
131
+        foreach($sections as $section) {
132
+            if (isset($this->parsers[$section])) {
133
+                $output[$section] = array();
134
+                foreach($this->parsers[$section] as $index => $parser) {
135
+                    $output[$section][$index] = $parser->output_raw();
136
+                }
137
+            }
138
+        }
139
+        return $output;
140
+    }
141
+
142
+    // strip escaped html tags
143
+    // this is not done for security, just to beautify things (html should already be escaped)
144
+    public static function strip_escaped_tags($string) {
145
+        // use the php function first
146
+        $string = strip_tags($string);
147
+
148
+        $gt = '&gt;|&#62;|>';
149
+        $lt = '&lt;|&#60;|<';
150
+
151
+        // there is no opening tag, so don't go through the rest of the regexes
152
+        if (!preg_match("($lt)",$string))
153
+            return $string;
154
+
155
+        $tag_match = "/(?:$lt)(?P<tag>(?:(?!$gt).)*)(?:$gt)/ims";
156
+
157
+        // this should match and remove tags
158
+        $string = preg_replace($tag_match,'',$string);
159
+
160
+        return $string;
161
+    }
162
+
163
+    protected function new_mediainfo() {
164
+        $this->output .= $this->output(false);
165
+        $this->parsed_lines = array();
166
+        foreach(array_keys($this->parsers) as $key) {
167
+            if ($key != static::GENERIC_PARSER)
168
+                unset($this->parsers[$key]);
169
+        }
170
+    }
171
+
172
+}
173
+
174
+class SectionParser {
175
+    protected $lines;
176
+    protected $index;
177
+
178
+
179
+    public function __construct(&$lines=array(),&$i=0) {
180
+        $this->set_lines($lines,$i);
181
+    }
182
+    public function set_lines(&$lines=array(),&$i=0){
183
+        $this->lines = &$lines;
184
+        $this->index = &$i;
185
+    }
186
+    // should always return the read line
187
+    public function parse_line(){
188
+        if (!isset($this->lines[$this->index])) {
189
+            return null;
190
+        }
191
+        $line = $this->lines[$this->index++];
192
+        $pair = static::property_value_pair($line);
193
+        $this->handle_cases($pair['property'],$pair['value']);
194
+        return $line;
195
+    }
196
+    public function output() {
197
+
198
+    }
199
+    protected function handle_cases($property, $value) {
200
+
201
+    }
202
+    public static function section_name($string) {
203
+        if (!$string) return false;
204
+        $mistart="/^(?:general$|unique id|complete name)/i";
205
+        if (preg_match($mistart, $string)) return ParseManager::MEDIAINFO_START;
206
+        $words = explode(' ',$string);
207
+        return strtolower($words[0]);
208
+    }
209
+    public static function property_value_pair($string) {
210
+        $pair = explode(":", $string, 2);
211
+        return array('property'=>strtolower(trim($pair[0])),'value'=>trim($pair[1]??''));
212
+    }
213
+    public static function strip_path($string) { // remove filepath
214
+        $string = str_replace("\\", "/", $string);
215
+        $path_parts = pathinfo($string);
216
+        return $path_parts['basename'];
217
+    }
218
+    public static function parse_size($string) {
219
+        return str_replace(array('pixels', ' '), null, $string);
220
+    }
221
+    protected static function table_head($caption) {
222
+        return "<table class='nobr'><caption>$caption</caption><tbody>";
223
+    }
224
+    protected static function table_row($property,$value) {
225
+        if ($value)
226
+            return "<tr><td>$property:&nbsp;&nbsp;</td><td>$value</td></tr>";
227
+        return '';
228
+    }
229
+    protected static function table_tail(){
230
+        return '</tbody></table>';
231
+    }
232
+
233
+}
234
+
235
+class AudioSectionParser extends SectionParser {
236
+    protected $audioformat;
237
+    protected $audiobitrate;
238
+    protected $audiochannels;
239
+    protected $audiochannelpositions;
240
+    protected $audiotitle;
241
+    protected $audiolang;
242
+    protected $audioprofile;
243
+    protected $form_audioformat;
244
+    protected $form_audiochannels;
245
+
246
+    protected function handle_cases($property,$value) {
247
+        switch ($property) {
248
+            case "format":
249
+                $this->audioformat = $value;
250
+                break;
251
+            case "bit rate":
252
+                $this->audiobitrate = $value;
253
+                break;
254
+            case "channel(s)":
255
+                $this->audiochannels = $value;
256
+                break;
257
+            case "channel positions":
258
+                $this->audiochannelpositions = $value;
259
+                break;
260
+            case "title":
261
+                $this->audiotitle = $value;
262
+                break;
263
+            case "language":
264
+                $this->audiolang = $value;
265
+                break;
266
+            case "format profile":
267
+                $this->audioprofile = $value;
268
+                break;
269
+        }
270
+    }
271
+    public function output() {
272
+        $this->process_vars();
273
+        $output = $this->audiolang . ' ' . $this->channels() . ' ' . $this->format();
274
+        $output .= ($this->audiobitrate) ? " @ $this->audiobitrate" : '';
275
+        $output .= ($this->audiotitle) ? " ($this->audiotitle)" : '';
276
+        return $output;
277
+    }
278
+    public function output_raw() {
279
+        $this->process_vars();
280
+        $output = array();
281
+        $properties = [
282
+            'audioformat', 'audiobitrate', 'audiochannels',
283
+            'audiochannelpositions', 'audiotitle', 'audiolang', 'audioprofile',
284
+            'form_audioformat', 'form_audiochannels'
285
+        ];
286
+        foreach($properties as $property) {
287
+            if ($this->$property) $output[$property] = $this->$property;
288
+        }
289
+        return $output;
290
+    }
291
+    protected function process_vars() {
292
+        $this->form_audioformat = $this->form_format();
293
+        $this->form_audiochannels = $this->form_channels();
294
+    }
295
+    protected function format() {
296
+        if (strtolower($this->audioformat) === 'mpeg audio') {
297
+            switch (strtolower($this->audioprofile)) {
298
+                case 'layer 3':
299
+                    return 'MP3';
300
+                case 'layer 2':
301
+                    return 'MP2';
302
+                case 'layer 1':
303
+                    return 'MP1';
304
+            }
305
+        }
306
+        return $this->audioformat;
307
+    }
308
+    protected function form_format() {
309
+        // Not implemented: Real Audio, DTS-HD
310
+        switch (strtolower($this->format())) {
311
+            case 'mp2':
312
+                return 'MP2';
313
+            case 'mp3':
314
+                return 'MP3';
315
+            case 'vorbis':
316
+                return 'OGG';
317
+            case 'aac':
318
+                return 'AAC';
319
+            case 'ac-3':
320
+                return 'AC3';
321
+            case 'truehd':
322
+                return 'TrueHD';
323
+            case 'dts':
324
+                switch (strtolower($this->audioprofile)) {
325
+                    case 'es':
326
+                        return 'DTS-ES';
327
+                    case 'ma / core':
328
+                        return 'DTS-HD MA';
329
+                    default:
330
+                        return 'DTS';
331
+                }
332
+            case 'flac':
333
+                return 'FLAC';
334
+            case 'pcm':
335
+                return 'PCM';
336
+            case 'wma':
337
+                return 'WMA';
338
+        }
339
+    }
340
+    protected function channels() {
341
+        if (isset($this->audiochannels)) {
342
+            $chans = preg_replace('/^(\d).*$/', '$1', $this->audiochannels);
343
+
344
+            if (isset($this->audiochannelpositions) && preg_match('/LFE/',
345
+                    $this->audiochannelpositions)) {
346
+                $chans -= .9;
347
+            } else {
348
+                $chans = $chans . '.0';
349
+            }
350
+
351
+            return $chans . 'ch';
352
+        }
353
+    }
354
+    protected function form_channels() {
355
+        return preg_replace('/ch/', '', $this->channels());
356
+    }
357
+}
358
+
359
+class GeneralSectionParser extends SectionParser {
360
+    public $filename;
361
+    protected $generalformat;
362
+    protected $duration;
363
+    protected $filesize;
364
+    protected $form_codec;
365
+    protected $form_releasegroup;
366
+
367
+    protected function handle_cases($property,$value) {
368
+        switch ($property) {
369
+            case "complete name":
370
+                // Remove autodetected urls
371
+                $value = preg_replace('#\[autourl(?:=.+)?\](.+)\[/autourl\]#', '$1', $value);
372
+                $this->filename = static::strip_path($value);
373
+                $this->lines[$this->index-1] = "Complete name : " . $this->filename;
374
+                if (strlen($this->filename) > 100)
375
+                    $this->filename = substr($this->filename, 0, 80) . '...' . substr($this->filename, -17);
376
+                break;
377
+            case "format":
378
+                $this->generalformat = $value;
379
+                break;
380
+            case "duration":
381
+                $this->duration = $value;
382
+                break;
383
+            case "file size":
384
+                $this->filesize = $value;
385
+                break;
386
+        }
387
+    }
388
+    public function output() {
389
+        $this->process_vars();
390
+        $output = static::table_head('General');
391
+        $properties = [
392
+            'Container' => 'generalformat',
393
+            'Runtime' => 'duration',
394
+            'Size' => 'filesize'
395
+        ];
396
+        foreach($properties as $property => $value) {
397
+            $output .= static::table_row($property,$this->$value);
398
+        }
399
+        $output .= static::table_tail();
400
+        return  $output;
401
+    }
402
+    public function output_raw() {
403
+        $this->process_vars();
404
+        $output = array();
405
+        $properties = [
406
+            'filename', 'generalformat', 'duration', 'filesize', 'form_codec',
407
+            'form_releasegroup'
408
+        ];
409
+        foreach($properties as $property) {
410
+            if ($this->$property) $output[$property] = $this->$property;
411
+        }
412
+        return $output;
413
+    }
414
+    protected function process_vars() {
415
+        switch (strtolower($this->generalformat)) {
416
+            case 'mpeg-ts':
417
+                $this->form_codec = 'MPEG-TS';
418
+                break;
419
+            // We can't determine if it's DVD5 or DVD9, so don't guess.
420
+            case 'mpeg-ps':
421
+                $this->form_codec = '---';
422
+                break;
423
+        }
424
+        $matches = array();
425
+        preg_match('/(?:^|.*\\|\/)\[(.*?)\].*$/',
426
+            $this->filename, $matches);
427
+        $this->form_releasegroup = $matches ? $matches[1] : '';
428
+    }
429
+}
430
+
431
+class TextSectionParser extends SectionParser {
432
+    protected $title;
433
+    protected $language;
434
+    protected $format;
435
+    protected $default;
436
+    protected $processed_language;
437
+    protected $form_format;
438
+
439
+    protected function handle_cases($property,$value) {
440
+        switch ($property) {
441
+            case 'title':
442
+                $this->title = $value;
443
+                break;
444
+            case 'language':
445
+                $this->language = $value;
446
+                break;
447
+            case 'format':
448
+                $this->format = $value;
449
+                break;
450
+            case 'default':
451
+                $this->default = ($value == 'Yes');
452
+                break;
453
+        }
454
+    }
455
+    public function output() {
456
+        $this->process_vars();
457
+        $language = $this->processed_language;
458
+        $output = "$language ($this->format)";
459
+        if ($this->title) $output .= " ($this->title)";
460
+        if ($this->default) $output .= ' (default)';
461
+        return $output;
462
+    }
463
+    public function output_raw() {
464
+        $this->process_vars();
465
+        $output = array();
466
+        $properties = [
467
+            'title', 'language', 'format', 'default', 'processed_language',
468
+            'form_format'
469
+        ];
470
+        foreach($properties as $property) {
471
+            if ($this->$property) $output[$property] = $this->$property;
472
+        }
473
+        return $output;
474
+    }
475
+    protected function process_vars() {
476
+        $this->processed_language = ($this->language) ?
477
+            $this->language : 'Unknown';
478
+        $this->form_format = 'Softsubbed';
479
+    }
480
+}
481
+
482
+class VideoSectionParser extends SectionParser {
483
+    protected $videoformat;
484
+    protected $videoformatversion;
485
+    protected $codec;
486
+    protected $width;
487
+    protected $height;
488
+    protected $writinglibrary;
489
+    protected $frameratemode;
490
+    protected $framerate;
491
+    protected $aspectratio;
492
+    protected $bitrate;
493
+    protected $bitratemode;
494
+    protected $nominalbitrate;
495
+    protected $bpp;
496
+    protected $bitdepth;
497
+    protected $processed_codec;
498
+    protected $processed_resolution;
499
+    protected $processed_framerate;
500
+    protected $form_codec;
501
+    protected $form_resolution;
502
+
503
+    protected function handle_cases($property,$value) {
504
+        switch ($property) {
505
+            case "format":
506
+                $this->videoformat = $value;
507
+                break;
508
+            case "format version":
509
+                $this->videoformatversion = $value;
510
+                break;
511
+            case "codec id":
512
+                $this->codec = strtolower($value);
513
+                break;
514
+            case "width":
515
+                $this->width = static::parse_size($value);
516
+                break;
517
+            case "height":
518
+                $this->height = static::parse_size($value);
519
+                break;
520
+            case "writing library":
521
+                $this->writinglibrary = $value;
522
+                break;
523
+            case "frame rate mode":
524
+                $this->frameratemode = $value;
525
+                break;
526
+            case "frame rate":
527
+                // if variable this becomes Original frame rate
528
+                $this->framerate = $value;
529
+                break;
530
+            case "display aspect ratio":
531
+                $this->aspectratio = $value;
532
+                break;
533
+            case "bit rate":
534
+                $this->bitrate = $value;
535
+                break;
536
+            case "bit rate mode":
537
+                $this->bitratemode = $value;
538
+                break;
539
+            case "nominal bit rate":
540
+                $this->nominalbitrate = $value;
541
+                break;
542
+            case "bits/(pixel*frame)":
543
+                $this->bpp = $value;
544
+                break;
545
+            case 'bit depth':
546
+                $this->bitdepth = $value;
547
+                break;
548
+        }
549
+    }
550
+    public function output() {
551
+        $this->process_vars();
552
+        $output = static::table_head('Video');
553
+        $properties = [
554
+            'Codec' => 'processed_codec',
555
+            'Bit depth' => 'bitdepth',
556
+            'Resolution' => 'processed_resolution',
557
+            'Aspect ratio' => 'aspectratio',
558
+            'Frame rate' => 'processed_framerate',
559
+            'Bit rate' => 'bitrate',
560
+            'BPP' => 'bpp'
561
+        ];
562
+        foreach($properties as $property => $value) {
563
+            $output .= static::table_row($property,$this->$value);
564
+        }
565
+        $output .= static::table_tail();
566
+        return  $output;
567
+    }
568
+    public function output_raw() {
569
+        $this->process_vars();
570
+        $output = array();
571
+        $properties = [
572
+            'videoformat', 'videoformatversion', 'codec', 'width', 'height',
573
+            'writinglibrary', 'frameratemode', 'framerate', 'aspectratio',
574
+            'bitrate', 'bitratemode', 'nominalbitrate', 'bpp', 'bitdepth',
575
+            'processed_codec', 'processed_resolution', 'processed_framerate',
576
+            'form_codec', 'form_resolution'
577
+        ];
578
+        foreach($properties as $property) {
579
+            if ($this->$property) $output[$property] = $this->$property;
580
+        }
581
+        return $output;
582
+    }
583
+    protected function process_vars() {
584
+        $this->processed_codec = $this->compute_codec();
585
+        $this->processed_resolution = ($this->width) ?
586
+            $this->width . 'x' . $this->height : '';
587
+        $this->processed_framerate = (strtolower($this->frameratemode) !=
588
+            "constant" && $this->frameratemode) ?
589
+            $this->frameratemode : $this->framerate;
590
+        $this->form_codec = $this->compute_form_codec();
591
+        $this->form_resolution = $this->compute_form_resolution();
592
+    }
593
+    protected function compute_codec() {
594
+        switch (strtolower($this->videoformat)) {
595
+            case "mpeg video":
596
+                switch (strtolower($this->videoformatversion)) {
597
+                    case "version 2":
598
+                        return "MPEG-2";
599
+                    case "version 1":
600
+                        return "MPEG-1";
601
+                }
602
+                return $this->videoformat;
603
+        }
604
+
605
+        switch (strtolower($this->codec)) {
606
+            case "div3":
607
+                return "DivX 3";
608
+            case "divx":
609
+            case "dx50":
610
+                return "DivX";
611
+            case "xvid":
612
+                return "XviD";
613
+            case "x264":
614
+                return "x264";
615
+        }
616
+
617
+        $chk = strtolower($this->codec);
618
+        $wl = strtolower($this->writinglibrary);
619
+        if (($chk === "v_mpeg4/iso/avc" || $chk === "avc1") && strpos($wl, "x264 core") === FALSE) {
620
+            return "H264";
621
+        } else if (($chk === "v_mpeg4/iso/avc" || $chk === "avc1") && strpos($wl, "x264 core") > -1)  {
622
+            return "x264";
623
+        } else if (strtolower($this->videoformat) === "avc" && strpos($wl, "x264 core") === FALSE) {
624
+            return "H264";
625
+        }
626
+
627
+        if (($chk === 'v_mpegh/iso/hevc') || ($wl === 'hevc'))
628
+            return 'H265';
629
+    }
630
+    protected function compute_form_codec() {
631
+        // Not implemented: DVD5, DVD9, WMV, Real Video
632
+        // MPEG-TS set as GeneralSectionParser::$form_codec if found
633
+        // MPEG-PS sets GeneralSectionParser::$form_codec to blank form value
634
+        //   so DVD5 or DVD9 is selected manually.
635
+        $codec = $this->compute_codec();
636
+        switch(strtolower($codec)) {
637
+            case 'x264':
638
+            case 'h264':
639
+                return strtolower($this->bitdepth) == '10 bits' ?
640
+                    'h264 10-bit' : 'h264';
641
+            case 'h265':
642
+                return 'h265';
643
+            case 'xvid':
644
+                return 'XviD';
645
+            case 'divx':
646
+            case 'divx 3':
647
+                return 'DivX';
648
+            case 'mpeg-1':
649
+                return 'MPEG';
650
+            case 'mpeg-2':
651
+                return 'MPEG-2';
652
+        }
653
+        switch(strtolower($this->codec)) {
654
+            case 'wmv3':
655
+                return 'VC-1';
656
+            case 'mp43':
657
+                return 'MPEG-4 v3';
658
+        }
659
+        switch(strtolower($this->videoformat)) {
660
+            case 'vc-1':
661
+                return 'VC-1';
662
+            case 's-mpeg 4 v3':
663
+                return 'MPEG-4 v3';
664
+        }
665
+    }
666
+    protected function compute_form_resolution() {
667
+				global $Resolutions;
668
+        $closest = null;
669
+        if (isset($this->height)) {
670
+            $resolutions = $Resolutions;
671
+            foreach($resolutions as $resolution) {
672
+                if (!isset($closest) || abs($this->height - $resolution) <
673
+                        abs($this->height - $closest)) {
674
+                    $closest = $resolution;
675
+                }
676
+            }
677
+        }
678
+        return $closest;
679
+    }
680
+}

+ 545
- 0
classes/misc.class.php View File

1
+<?
2
+class Misc {
3
+	/**
4
+	 * Send an email.
5
+	 *
6
+	 * @param string $To the email address to send it to.
7
+	 * @param string $Subject
8
+	 * @param string $Body
9
+	 * @param string $From The user part of the user@NONSSL_SITE_URL email address.
10
+	 * @param string $ContentType text/plain or text/html
11
+	 */
12
+	public static function send_email($To, $Subject, $Body, $From = 'noreply', $ContentType = 'text/plain') {
13
+		$Headers = 'MIME-Version: 1.0'."\r\n";
14
+		$Headers .= 'Content-type: '.$ContentType.'; charset=iso-8859-1'."\r\n";
15
+		$Headers .= 'From: '.SITE_NAME.' <'.$From.'@'.NONSSL_SITE_URL.'>'."\r\n";
16
+		$Headers .= 'Reply-To: '.$From.'@'.NONSSL_SITE_URL."\r\n";
17
+		$Headers .= 'X-Mailer: Project Gazelle'."\r\n";
18
+		$Headers .= 'Message-Id: <'.Users::make_secret().'@'.NONSSL_SITE_URL.">\r\n";
19
+		$Headers .= 'X-Priority: 3'."\r\n";
20
+		mail($To, $Subject, $Body, $Headers, "-f $From@".NONSSL_SITE_URL);
21
+	}
22
+
23
+
24
+	/**
25
+	 * Sanitize a string to be allowed as a filename.
26
+	 *
27
+	 * @param string $EscapeStr the string to escape
28
+	 * @return the string with all banned characters removed.
29
+	 */
30
+	public static function file_string($EscapeStr) {
31
+		return str_replace(array('"', '*', '/', ':', '<', '>', '?', '\\', '|'), '', $EscapeStr);
32
+	}
33
+
34
+
35
+	/**
36
+	 * Sends a PM from $FromId to $ToId.
37
+	 *
38
+	 * @param string $ToID ID of user to send PM to. If $ToID is an array and $ConvID is empty, a message will be sent to multiple users.
39
+	 * @param string $FromID ID of user to send PM from, 0 to send from system
40
+	 * @param string $Subject
41
+	 * @param string $Body
42
+	 * @param int $ConvID The conversation the message goes in. Leave blank to start a new conversation.
43
+	 * @return
44
+	 */
45
+	public static function send_pm($ToID, $FromID, $Subject, $Body, $ConvID = '') {
46
+		global $Time;
47
+		$UnescapedSubject = $Subject;
48
+		$UnescapedBody = $Body;
49
+		$Subject = db_string($Subject);
50
+		$Body = DBCrypt::encrypt($Body);
51
+		if ($ToID == 0) {
52
+			// Don't allow users to send messages to the system
53
+			return;
54
+		}
55
+
56
+		$QueryID = G::$DB->get_query_id();
57
+
58
+		if ($ConvID == '') {
59
+			// Create a new conversation.
60
+			G::$DB->query("
61
+				INSERT INTO pm_conversations (Subject)
62
+				VALUES ('$Subject')");
63
+			$ConvID = G::$DB->inserted_id();
64
+			G::$DB->query("
65
+				INSERT INTO pm_conversations_users
66
+					(UserID, ConvID, InInbox, InSentbox, SentDate, ReceivedDate, UnRead)
67
+				VALUES
68
+					('$ToID', '$ConvID', '1','0','".sqltime()."', '".sqltime()."', '1')");
69
+      if ($FromID == $ToID) {
70
+        G::$DB->query("
71
+          UPDATE pm_conversations_users
72
+            SET InSentbox = '1'
73
+            WHERE ConvID = '$ConvID'"
74
+        );
75
+      }
76
+      elseif ($FromID != 0) {
77
+				G::$DB->query("
78
+					INSERT INTO pm_conversations_users
79
+						(UserID, ConvID, InInbox, InSentbox, SentDate, ReceivedDate, UnRead)
80
+					VALUES
81
+						('$FromID', '$ConvID', '0','1','".sqltime()."', '".sqltime()."', '0')");
82
+			}
83
+			$ToID = array($ToID);
84
+		} else {
85
+			// Update the pre-existing conversations.
86
+			G::$DB->query("
87
+				UPDATE pm_conversations_users
88
+				SET
89
+					InInbox = '1',
90
+					UnRead = '1',
91
+					ReceivedDate = '".sqltime()."'
92
+				WHERE UserID IN (".implode(',', $ToID).")
93
+					AND ConvID = '$ConvID'");
94
+
95
+			G::$DB->query("
96
+				UPDATE pm_conversations_users
97
+				SET
98
+					InSentbox = '1',
99
+					SentDate = '".sqltime()."'
100
+				WHERE UserID = '$FromID'
101
+					AND ConvID = '$ConvID'");
102
+		}
103
+
104
+		// Now that we have a $ConvID for sure, send the message.
105
+		G::$DB->query("
106
+			INSERT INTO pm_messages
107
+				(SenderID, ConvID, SentDate, Body)
108
+			VALUES
109
+				('$FromID', '$ConvID', '".sqltime()."', '$Body')");
110
+
111
+		// Update the cached new message count.
112
+		foreach ($ToID as $ID) {
113
+			G::$DB->query("
114
+				SELECT COUNT(ConvID)
115
+				FROM pm_conversations_users
116
+				WHERE UnRead = '1'
117
+					AND UserID = '$ID'
118
+					AND InInbox = '1'");
119
+			list($UnRead) = G::$DB->next_record();
120
+			G::$Cache->cache_value("inbox_new_$ID", $UnRead);
121
+		}
122
+
123
+		G::$DB->query("
124
+			SELECT Username
125
+			FROM users_main
126
+			WHERE ID = '$FromID'");
127
+		list($SenderName) = G::$DB->next_record();
128
+		foreach ($ToID as $ID) {
129
+			G::$DB->query("
130
+				SELECT COUNT(ConvID)
131
+				FROM pm_conversations_users
132
+				WHERE UnRead = '1'
133
+					AND UserID = '$ID'
134
+					AND InInbox = '1'");
135
+			list($UnRead) = G::$DB->next_record();
136
+			G::$Cache->cache_value("inbox_new_$ID", $UnRead);
137
+
138
+			NotificationsManager::send_push($ID, "Message from $SenderName, Subject: $UnescapedSubject", $UnescapedBody, site_url() . 'inbox.php', NotificationsManager::INBOX);
139
+		}
140
+
141
+		G::$DB->set_query_id($QueryID);
142
+
143
+		return $ConvID;
144
+	}
145
+
146
+	/**
147
+	 * Create thread function, things should already be escaped when sent here.
148
+	 *
149
+	 * @param int $ForumID
150
+	 * @param int $AuthorID ID of the user creating the post.
151
+	 * @param string $Title
152
+	 * @param string $PostBody
153
+	 * @return -1 on error, -2 on user not existing, thread id on success.
154
+	 */
155
+	public static function create_thread($ForumID, $AuthorID, $Title, $PostBody) {
156
+		global $Time;
157
+		if (!$ForumID || !$AuthorID || !is_number($AuthorID) || !$Title || !$PostBody) {
158
+			return -1;
159
+		}
160
+
161
+		$QueryID = G::$DB->get_query_id();
162
+
163
+		G::$DB->query("
164
+			SELECT Username
165
+			FROM users_main
166
+			WHERE ID = $AuthorID");
167
+		if (!G::$DB->has_results()) {
168
+			G::$DB->set_query_id($QueryID);
169
+			return -2;
170
+		}
171
+		list($AuthorName) = G::$DB->next_record();
172
+
173
+		$ThreadInfo = array();
174
+		$ThreadInfo['IsLocked'] = 0;
175
+		$ThreadInfo['IsSticky'] = 0;
176
+
177
+		G::$DB->query("
178
+			INSERT INTO forums_topics
179
+				(Title, AuthorID, ForumID, LastPostTime, LastPostAuthorID, CreatedTime)
180
+			VALUES
181
+				('$Title', '$AuthorID', '$ForumID', '".sqltime()."', '$AuthorID', '".sqltime()."')");
182
+		$TopicID = G::$DB->inserted_id();
183
+		$Posts = 1;
184
+
185
+		G::$DB->query("
186
+			INSERT INTO forums_posts
187
+				(TopicID, AuthorID, AddedTime, Body)
188
+			VALUES
189
+				('$TopicID', '$AuthorID', '".sqltime()."', '$PostBody')");
190
+		$PostID = G::$DB->inserted_id();
191
+
192
+		G::$DB->query("
193
+			UPDATE forums
194
+			SET
195
+				NumPosts  = NumPosts + 1,
196
+				NumTopics = NumTopics + 1,
197
+				LastPostID = '$PostID',
198
+				LastPostAuthorID = '$AuthorID',
199
+				LastPostTopicID = '$TopicID',
200
+				LastPostTime = '".sqltime()."'
201
+			WHERE ID = '$ForumID'");
202
+
203
+		G::$DB->query("
204
+			UPDATE forums_topics
205
+			SET
206
+				NumPosts = NumPosts + 1,
207
+				LastPostID = '$PostID',
208
+				LastPostAuthorID = '$AuthorID',
209
+				LastPostTime = '".sqltime()."'
210
+			WHERE ID = '$TopicID'");
211
+
212
+		// Bump this topic to head of the cache
213
+		list($Forum,,, $Stickies) = G::$Cache->get_value("forums_$ForumID");
214
+		if (!empty($Forum)) {
215
+			if (count($Forum) == TOPICS_PER_PAGE && $Stickies < TOPICS_PER_PAGE) {
216
+				array_pop($Forum);
217
+			}
218
+			G::$DB->query("
219
+				SELECT IsLocked, IsSticky, NumPosts
220
+				FROM forums_topics
221
+				WHERE ID ='$TopicID'");
222
+			list($IsLocked, $IsSticky, $NumPosts) = G::$DB->next_record();
223
+			$Part1 = array_slice($Forum, 0, $Stickies, true); //Stickys
224
+			$Part2 = array(
225
+				$TopicID => array(
226
+					'ID' => $TopicID,
227
+					'Title' => $Title,
228
+					'AuthorID' => $AuthorID,
229
+					'IsLocked' => $IsLocked,
230
+					'IsSticky' => $IsSticky,
231
+					'NumPosts' => $NumPosts,
232
+					'LastPostID' => $PostID,
233
+					'LastPostTime' => sqltime(),
234
+					'LastPostAuthorID' => $AuthorID,
235
+					)
236
+				); //Bumped thread
237
+			$Part3 = array_slice($Forum, $Stickies, TOPICS_PER_PAGE, true); //Rest of page
238
+			if ($Stickies > 0) {
239
+				$Part1 = array_slice($Forum, 0, $Stickies, true); //Stickies
240
+				$Part3 = array_slice($Forum, $Stickies, TOPICS_PER_PAGE - $Stickies - 1, true); //Rest of page
241
+			} else {
242
+				$Part1 = array();
243
+				$Part3 = $Forum;
244
+			}
245
+			if (is_null($Part1)) {
246
+				$Part1 = array();
247
+			}
248
+			if (is_null($Part3)) {
249
+				$Part3 = array();
250
+			}
251
+			$Forum = $Part1 + $Part2 + $Part3;
252
+			G::$Cache->cache_value("forums_$ForumID", array($Forum, '', 0, $Stickies), 0);
253
+		}
254
+
255
+		//Update the forum root
256
+		G::$Cache->begin_transaction('forums_list');
257
+		$UpdateArray = array(
258
+			'NumPosts' => '+1',
259
+			'NumTopics' => '+1',
260
+			'LastPostID' => $PostID,
261
+			'LastPostAuthorID' => $AuthorID,
262
+			'LastPostTopicID' => $TopicID,
263
+			'LastPostTime' => sqltime(),
264
+			'Title' => $Title,
265
+			'IsLocked' => $ThreadInfo['IsLocked'],
266
+			'IsSticky' => $ThreadInfo['IsSticky']
267
+			);
268
+
269
+		$UpdateArray['NumTopics'] = '+1';
270
+
271
+		G::$Cache->update_row($ForumID, $UpdateArray);
272
+		G::$Cache->commit_transaction(0);
273
+
274
+		$CatalogueID = floor((POSTS_PER_PAGE * ceil($Posts / POSTS_PER_PAGE) - POSTS_PER_PAGE) / THREAD_CATALOGUE);
275
+		G::$Cache->begin_transaction('thread_'.$TopicID.'_catalogue_'.$CatalogueID);
276
+		$Post = array(
277
+			'ID' => $PostID,
278
+			'AuthorID' => G::$LoggedUser['ID'],
279
+			'AddedTime' => sqltime(),
280
+			'Body' => $PostBody,
281
+			'EditedUserID' => 0,
282
+			'EditedTime' => '0000-00-00 00:00:00',
283
+			'Username' => ''
284
+			);
285
+		G::$Cache->insert('', $Post);
286
+		G::$Cache->commit_transaction(0);
287
+
288
+		G::$Cache->begin_transaction('thread_'.$TopicID.'_info');
289
+		G::$Cache->update_row(false, array('Posts' => '+1', 'LastPostAuthorID' => $AuthorID));
290
+		G::$Cache->commit_transaction(0);
291
+
292
+		G::$DB->set_query_id($QueryID);
293
+
294
+		return $TopicID;
295
+	}
296
+
297
+	/**
298
+	 * If the suffix of $Haystack is $Needle
299
+	 *
300
+	 * @param string $Haystack String to search in
301
+	 * @param string $Needle String to search for
302
+	 * @return boolean True if $Needle is a suffix of $Haystack
303
+	 */
304
+	public static function ends_with($Haystack, $Needle) {
305
+		return substr($Haystack, strlen($Needle) * -1) == $Needle;
306
+	}
307
+
308
+
309
+	/**
310
+	 * If the prefix of $Haystack is $Needle
311
+	 *
312
+	 * @param string $Haystack String to search in
313
+	 * @param string $Needle String to search for
314
+	 * @return boolean True if $Needle is a prefix of $Haystack
315
+	 */
316
+	public static function starts_with($Haystack, $Needle) {
317
+		return strpos($Haystack, $Needle) === 0;
318
+	}
319
+
320
+	/**
321
+	 * Variant of in_array() with trailing wildcard support
322
+	 *
323
+	 * @param string $Needle, array $Haystack
324
+	 * @return boolean true if (substring of) $Needle exists in $Haystack
325
+	 */
326
+	public static function in_array_partial($Needle, $Haystack) {
327
+		static $Searches = array();
328
+		if (array_key_exists($Needle, $Searches)) {
329
+			return $Searches[$Needle];
330
+		}
331
+		foreach ($Haystack as $String) {
332
+			if (substr($String, -1) == '*') {
333
+				if (!strncmp($Needle, $String, strlen($String) - 1)) {
334
+					$Searches[$Needle] = true;
335
+					return true;
336
+				}
337
+			} elseif (!strcmp($Needle, $String)) {
338
+				$Searches[$Needle] = true;
339
+				return true;
340
+			}
341
+		}
342
+		$Searches[$Needle] = false;
343
+		return false;
344
+	}
345
+
346
+	/**
347
+	 * Used to check if keys in $_POST and $_GET are all set, and throws an error if not.
348
+	 * This reduces 'if' statement redundancy for a lot of variables
349
+	 *
350
+	 * @param array $Request Either $_POST or $_GET, or whatever other array you want to check.
351
+	 * @param array $Keys The keys to ensure are set.
352
+	 * @param boolean $AllowEmpty If set to true, a key that is in the request but blank will not throw an error.
353
+	 * @param int $Error The error code to throw if one of the keys isn't in the array.
354
+	 */
355
+	public static function assert_isset_request($Request, $Keys = null, $AllowEmpty = false, $Error = 0) {
356
+		if (isset($Keys)) {
357
+			foreach ($Keys as $K) {
358
+				if (!isset($Request[$K]) || ($AllowEmpty == false && $Request[$K] == '')) {
359
+					error($Error);
360
+					break;
361
+				}
362
+			}
363
+		} else {
364
+			foreach ($Request as $R) {
365
+				if (!isset($R) || ($AllowEmpty == false && $R == '')) {
366
+					error($Error);
367
+					break;
368
+				}
369
+			}
370
+		}
371
+	}
372
+
373
+
374
+	/**
375
+	 * Given an array of tags, return an array of their IDs.
376
+	 *
377
+	 * @param array $TagNames
378
+	 * @return array IDs
379
+	 */
380
+	public static function get_tags($TagNames) {
381
+		$TagIDs = array();
382
+		foreach ($TagNames as $Index => $TagName) {
383
+			$Tag = G::$Cache->get_value("tag_id_$TagName");
384
+			if (is_array($Tag)) {
385
+				unset($TagNames[$Index]);
386
+				$TagIDs[$Tag['ID']] = $Tag['Name'];
387
+			}
388
+		}
389
+		if (count($TagNames) > 0) {
390
+			$QueryID = G::$DB->get_query_id();
391
+			G::$DB->query("
392
+				SELECT ID, Name
393
+				FROM tags
394
+				WHERE Name IN ('".implode("', '", $TagNames)."')");
395
+			$SQLTagIDs = G::$DB->to_array();
396
+			G::$DB->set_query_id($QueryID);
397
+			foreach ($SQLTagIDs as $Tag) {
398
+				$TagIDs[$Tag['ID']] = $Tag['Name'];
399
+				G::$Cache->cache_value('tag_id_'.$Tag['Name'], $Tag, 0);
400
+			}
401
+		}
402
+
403
+		return($TagIDs);
404
+	}
405
+
406
+
407
+	/**
408
+	 * Gets the alias of the tag; if there is no alias, silently returns the original tag.
409
+	 *
410
+	 * @param string $BadTag the tag we want to alias
411
+	 * @return string The aliased tag.
412
+	 */
413
+	public static function get_alias_tag($BadTag) {
414
+		$QueryID = G::$DB->get_query_id();
415
+		G::$DB->query("
416
+			SELECT AliasTag
417
+			FROM tag_aliases
418
+			WHERE BadTag = '$BadTag'
419
+			LIMIT 1");
420
+		if (G::$DB->has_results()) {
421
+			list($AliasTag) = G::$DB->next_record();
422
+		} else {
423
+			$AliasTag = $BadTag;
424
+		}
425
+		G::$DB->set_query_id($QueryID);
426
+		return $AliasTag;
427
+	}
428
+
429
+
430
+	/*
431
+	 * Write a message to the system log.
432
+	 *
433
+	 * @param string $Message the message to write.
434
+	 */
435
+	public static function write_log($Message) {
436
+		global $Time;
437
+		$QueryID = G::$DB->get_query_id();
438
+		G::$DB->query("
439
+			INSERT INTO log (Message, Time)
440
+			VALUES ('" . db_string($Message) . "', '" . sqltime() . "')");
441
+		G::$DB->set_query_id($QueryID);
442
+	}
443
+
444
+
445
+	/**
446
+	 * Get a tag ready for database input and display.
447
+	 *
448
+	 * @param string $Str
449
+	 * @return sanitized version of $Str
450
+	 */
451
+	public static function sanitize_tag($Str) {
452
+		$Str = strtolower($Str);
453
+		$Str = preg_replace('/[^a-z0-9.]/', '', $Str);
454
+		$Str = preg_replace('/(^[.,]*)|([.,]*$)/', '', $Str);
455
+		$Str = htmlspecialchars($Str);
456
+		$Str = db_string(trim($Str));
457
+		return $Str;
458
+	}
459
+
460
+	/**
461
+	 * HTML escape an entire array for output.
462
+	 * @param array $Array, what we want to escape
463
+	 * @param boolean/array $Escape
464
+	 *	if true, all keys escaped
465
+	 *	if false, no escaping.
466
+	 *	If array, it's a list of array keys not to escape.
467
+	 * @return mutated version of $Array with values escaped.
468
+	 */
469
+	public static function display_array($Array, $Escape = array()) {
470
+		foreach ($Array as $Key => $Val) {
471
+			if ((!is_array($Escape) && $Escape == true) || !in_array($Key, $Escape)) {
472
+				$Array[$Key] = display_str($Val);
473
+			}
474
+		}
475
+		return $Array;
476
+	}
477
+
478
+	/**
479
+	 * Searches for a key/value pair in an array.
480
+	 *
481
+	 * @return array of results
482
+	 */
483
+	public static function search_array($Array, $Key, $Value) {
484
+		$Results = array();
485
+		if (is_array($Array))
486
+		{
487
+			if (isset($Array[$Key]) && $Array[$Key] == $Value) {
488
+				$Results[] = $Array;
489
+			}
490
+
491
+			foreach ($Array as $subarray) {
492
+				$Results = array_merge($Results, self::search_array($subarray, $Key, $Value));
493
+			}
494
+		}
495
+		return $Results;
496
+	}
497
+
498
+	/**
499
+	 * Search for $Needle in the string $Haystack which is a list of values separated by $Separator.
500
+	 * @param string $Haystack
501
+	 * @param string $Needle
502
+	 * @param string $Separator
503
+	 * @param boolean $Strict
504
+	 * @return boolean
505
+	 */
506
+	public static function search_joined_string($Haystack, $Needle, $Separator = '|', $Strict = true) {
507
+		return (array_search($Needle, explode($Separator, $Haystack), $Strict) !== false);
508
+	}
509
+
510
+	/**
511
+	 * Check for a ":" in the beginning of a torrent meta data string
512
+	 * to see if it's stored in the old base64-encoded format
513
+	 *
514
+	 * @param string $Torrent the torrent data
515
+	 * @return true if the torrent is stored in binary format
516
+	 */
517
+	public static function is_new_torrent(&$Data) {
518
+		return strpos(substr($Data, 0, 10), ':') !== false;
519
+	}
520
+
521
+	public static function display_recommend($ID, $Type, $Hide = true) {
522
+		if ($Hide) {
523
+			$Hide = ' style="display: none;"';
524
+		}
525
+		?>
526
+		<div id="recommendation_div" data-id="<?=$ID?>" data-type="<?=$Type?>"<?=$Hide?> class="center">
527
+			<div style="display: inline-block;">
528
+				<strong>Recommend to:</strong>
529
+				<select id="friend" name="friend">
530
+					<option value="0" selected="selected">Choose friend</option>
531
+				</select>
532
+				<input type="text" id="recommendation_note" placeholder="Add note..." />
533
+				<button id="send_recommendation" disabled="disabled">Send</button>
534
+			</div>
535
+			<div class="new" id="recommendation_status"><br /></div>
536
+		</div>
537
+<?
538
+	}
539
+
540
+	public static function is_valid_url($URL) {
541
+		return preg_match('|^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$|i', $URL);
542
+	}
543
+
544
+}
545
+?>

+ 414
- 0
classes/mysql.class.php View File

1
+<?
2
+//-----------------------------------------------------------------------------------
3
+/////////////////////////////////////////////////////////////////////////////////////
4
+/*//-- MySQL wrapper class ----------------------------------------------------------
5
+
6
+This class provides an interface to mysqli. You should always use this class instead
7
+of the mysql/mysqli functions, because this class provides debugging features and a
8
+bunch of other cool stuff.
9
+
10
+Everything returned by this class is automatically escaped for output. This can be
11
+turned off by setting $Escape to false in next_record or to_array.
12
+
13
+//--------- Basic usage -------------------------------------------------------------
14
+
15
+* Creating the object.
16
+
17
+require(SERVER_ROOT.'/classes/mysql.class.php');
18
+$DB = NEW DB_MYSQL;
19
+-----
20
+
21
+* Making a query
22
+
23
+$DB->query("
24
+	SELECT *
25
+	FROM table...");
26
+
27
+	Is functionally equivalent to using mysqli_query("SELECT * FROM table...")
28
+	Stores the result set in $this->QueryID
29
+	Returns the result set, so you can save it for later (see set_query_id())
30
+-----
31
+
32
+* Getting data from a query
33
+
34
+$array = $DB->next_record();
35
+	Is functionally equivalent to using mysqli_fetch_array($ResultSet)
36
+	You do not need to specify a result set - it uses $this-QueryID
37
+-----
38
+
39
+* Escaping a string
40
+
41
+db_string($str);
42
+	Is a wrapper for $DB->escape_str(), which is a wrapper for
43
+	mysqli_real_escape_string(). The db_string() function exists so that you
44
+	don't have to keep calling $DB->escape_str().
45
+
46
+	USE THIS FUNCTION EVERY TIME YOU USE AN UNVALIDATED USER-SUPPLIED VALUE IN
47
+	A DATABASE QUERY!
48
+
49
+
50
+//--------- Advanced usage ---------------------------------------------------------
51
+
52
+* The conventional way of retrieving a row from a result set is as follows:
53
+
54
+list($All, $Columns, $That, $You, $Select) = $DB->next_record();
55
+-----
56
+
57
+* This is how you loop over the result set:
58
+
59
+while (list($All, $Columns, $That, $You, $Select) = $DB->next_record()) {
60
+	echo "Do stuff with $All of the ".$Columns.$That.$You.$Select;
61
+}
62
+-----
63
+
64
+* There are also a couple more mysqli functions that have been wrapped. They are:
65
+
66
+record_count()
67
+	Wrapper to mysqli_num_rows()
68
+
69
+affected_rows()
70
+	Wrapper to mysqli_affected_rows()
71
+
72
+inserted_id()
73
+	Wrapper to mysqli_insert_id()
74
+
75
+close
76
+	Wrapper to mysqli_close()
77
+-----
78
+
79
+* And, of course, a few handy custom functions.
80
+
81
+to_array($Key = false)
82
+	Transforms an entire result set into an array (useful in situations where you
83
+	can't order the rows properly in the query).
84
+
85
+	If $Key is set, the function uses $Key as the index (good for looking up a
86
+	field). Otherwise, it uses an iterator.
87
+
88
+	For an example of this function in action, check out forum.php.
89
+
90
+collect($Key)
91
+	Loops over the result set, creating an array from one of the fields ($Key).
92
+	For an example, see forum.php.
93
+
94
+set_query_id($ResultSet)
95
+	This class can only hold one result set at a time. Using set_query_id allows
96
+	you to set the result set that the class is using to the result set in
97
+	$ResultSet. This result set should have been obtained earlier by using
98
+	$DB->query().
99
+
100
+	Example:
101
+
102
+	$FoodRS = $DB->query("
103
+			SELECT *
104
+			FROM food");
105
+	$DB->query("
106
+		SELECT *
107
+		FROM drink");
108
+	$Drinks = $DB->next_record();
109
+	$DB->set_query_id($FoodRS);
110
+	$Food = $DB->next_record();
111
+
112
+	Of course, this example is contrived, but you get the point.
113
+
114
+
115
+-------------------------------------------------------------------------------------
116
+*///---------------------------------------------------------------------------------
117
+
118
+if (!extension_loaded('mysqli')) {
119
+	die('Mysqli Extension not loaded.');
120
+}
121
+
122
+//Handles escaping
123
+function db_string($String, $DisableWildcards = false) {
124
+	global $DB;
125
+	//Escape
126
+	$String = $DB->escape_str($String);
127
+	//Remove user input wildcards
128
+	if ($DisableWildcards) {
129
+		$String = str_replace(array('%','_'), array('\%','\_'), $String);
130
+	}
131
+	return $String;
132
+}
133
+
134
+function db_array($Array, $DontEscape = array(), $Quote = false) {
135
+	foreach ($Array as $Key => $Val) {
136
+		if (!in_array($Key, $DontEscape)) {
137
+			if ($Quote) {
138
+				$Array[$Key] = '\''.db_string(trim($Val)).'\'';
139
+			} else {
140
+				$Array[$Key] = db_string(trim($Val));
141
+			}
142
+		}
143
+	}
144
+	return $Array;
145
+}
146
+
147
+//TODO: revisit access levels once Drone is replaced by ZeRobot
148
+class DB_MYSQL {
149
+	public $LinkID = false;
150
+	protected $QueryID = false;
151
+	protected $Record = array();
152
+	protected $Row;
153
+	protected $Errno = 0;
154
+	protected $Error = '';
155
+
156
+	public $Queries = array();
157
+	public $Time = 0.0;
158
+
159
+	protected $Database = '';
160
+	protected $Server = '';
161
+	protected $User = '';
162
+	protected $Pass = '';
163
+	protected $Port = 0;
164
+	protected $Socket = '';
165
+
166
+	function __construct($Database = SQLDB, $User = SQLLOGIN, $Pass = SQLPASS, $Server = SQLHOST, $Port = SQLPORT, $Socket = SQLSOCK) {
167
+		$this->Database = $Database;
168
+		$this->Server = $Server;
169
+		$this->User = $User;
170
+		$this->Pass = $Pass;
171
+		$this->Port = $Port;
172
+		$this->Socket = $Socket;
173
+	}
174
+
175
+	function halt($Msg) {
176
+		global $Debug, $argv;
177
+		$DBError = 'MySQL: '.strval($Msg).' SQL error: '.strval($this->Errno).' ('.strval($this->Error).')';
178
+		if ($this->Errno == 1194) {
179
+			send_irc('PRIVMSG '.ADMIN_CHAN.' :'.$this->Error);
180
+		}
181
+		/*if ($this->Errno == 1194) {
182
+			preg_match("Table '(\S+)' is marked as crashed and should be repaired", $this->Error, $Matches);
183
+		} */
184
+		$Debug->analysis('!dev DB Error', $DBError, 3600 * 24);
185
+		if (DEBUG_MODE || check_perms('site_debug') || isset($argv[1])) {
186
+			echo '<pre>'.display_str($DBError).'</pre>';
187
+			if (DEBUG_MODE || check_perms('site_debug')) {
188
+				print_r($this->Queries);
189
+			}
190
+			die();
191
+		} else {
192
+			error('-1');
193
+		}
194
+	}
195
+
196
+	function connect() {
197
+		if (!$this->LinkID) {
198
+			$this->LinkID = mysqli_connect($this->Server, $this->User, $this->Pass, $this->Database, $this->Port, $this->Socket); // defined in config.php
199
+			if (!$this->LinkID) {
200
+				$this->Errno = mysqli_connect_errno();
201
+				$this->Error = mysqli_connect_error();
202
+				$this->halt('Connection failed (host:'.$this->Server.':'.$this->Port.')');
203
+			}
204
+		}
205
+	}
206
+
207
+	function query($Query, $AutoHandle = 1) {
208
+		global $Debug;
209
+		/*
210
+		 * If there was a previous query, we store the warnings. We cannot do
211
+		 * this immediately after mysqli_query because mysqli_insert_id will
212
+		 * break otherwise due to mysqli_get_warnings sending a SHOW WARNINGS;
213
+		 * query. When sending a query, however, we're sure that we won't call
214
+		 * mysqli_insert_id (or any similar function, for that matter) later on,
215
+		 * so we can safely get the warnings without breaking things.
216
+		 * Note that this means that we have to call $this->warnings manually
217
+		 * for the last query!
218
+		 */
219
+		if ($this->QueryID) {
220
+			$this->warnings();
221
+		}
222
+		$QueryStartTime = microtime(true);
223
+		$this->connect();
224
+		// In the event of a MySQL deadlock, we sleep allowing MySQL time to unlock, then attempt again for a maximum of 5 tries
225
+		for ($i = 1; $i < 6; $i++) {
226
+			$this->QueryID = mysqli_query($this->LinkID, $Query);
227
+			if (!in_array(mysqli_errno($this->LinkID), array(1213, 1205))) {
228
+				break;
229
+			}
230
+			$Debug->analysis('Non-Fatal Deadlock:', $Query, 3600 * 24);
231
+			trigger_error("Database deadlock, attempt $i");
232
+
233
+			sleep($i * rand(2, 5)); // Wait longer as attempts increase
234
+		}
235
+		$QueryEndTime = microtime(true);
236
+		$this->Queries[] = array($Query, ($QueryEndTime - $QueryStartTime) * 1000, null);
237
+		$this->Time += ($QueryEndTime - $QueryStartTime) * 1000;
238
+
239
+		if (!$this->QueryID) {
240
+			$this->Errno = mysqli_errno($this->LinkID);
241
+			$this->Error = mysqli_error($this->LinkID);
242
+
243
+			if ($AutoHandle) {
244
+				$this->halt("Invalid Query: $Query");
245
+			} else {
246
+				return $this->Errno;
247
+			}
248
+		}
249
+
250
+		/*
251
+		$QueryType = substr($Query, 0, 6);
252
+		if ($QueryType === 'DELETE' || $QueryType === 'UPDATE') {
253
+			if ($this->affected_rows() > 50) {
254
+				$Debug->analysis($this->affected_rows().' rows altered:', $Query, 3600 * 24);
255
+			}
256
+		}
257
+		*/
258
+		$this->Row = 0;
259
+		if ($AutoHandle) {
260
+			return $this->QueryID;
261
+		}
262
+	}
263
+
264
+	function query_unb($Query) {
265
+		$this->connect();
266
+		mysqli_real_query($this->LinkID, $Query);
267
+	}
268
+
269
+	function inserted_id() {
270
+		if ($this->LinkID) {
271
+			return mysqli_insert_id($this->LinkID);
272
+		}
273
+	}
274
+
275
+	function next_record($Type = MYSQLI_BOTH, $Escape = true) { // $Escape can be true, false, or an array of keys to not escape
276
+		if ($this->LinkID) {
277
+			$this->Record = mysqli_fetch_array($this->QueryID, $Type);
278
+			$this->Row++;
279
+			if (!is_array($this->Record)) {
280
+				$this->QueryID = false;
281
+			} elseif ($Escape !== false) {
282
+				$this->Record = Misc::display_array($this->Record, $Escape);
283
+			}
284
+			return $this->Record;
285
+		}
286
+	}
287
+
288
+	function close() {
289
+		if ($this->LinkID) {
290
+			if (!mysqli_close($this->LinkID)) {
291
+				$this->halt('Cannot close connection or connection did not open.');
292
+			}
293
+			$this->LinkID = false;
294
+		}
295
+	}
296
+
297
+	/*
298
+	 * returns an integer with the number of rows found
299
+	 * returns a string if the number of rows found exceeds MAXINT
300
+	 */
301
+	function record_count() {
302
+		if ($this->QueryID) {
303
+			return mysqli_num_rows($this->QueryID);
304
+		}
305
+	}
306
+
307
+	/*
308
+	 * returns true if the query exists and there were records found
309
+	 * returns false if the query does not exist or if there were 0 records returned
310
+	 */
311
+	function has_results() {
312
+		return ($this->QueryID && $this->record_count() !== 0);
313
+	}
314
+
315
+	function affected_rows() {
316
+		if ($this->LinkID) {
317
+			return mysqli_affected_rows($this->LinkID);
318
+		}
319
+	}
320
+
321
+	function info() {
322
+		return mysqli_get_host_info($this->LinkID);
323
+	}
324
+
325
+	// You should use db_string() instead.
326
+	function escape_str($Str) {
327
+		$this->connect(0);
328
+		if (is_array($Str)) {
329
+			trigger_error('Attempted to escape array.');
330
+			return '';
331
+		}
332
+		return mysqli_real_escape_string($this->LinkID, $Str);
333
+	}
334
+
335
+	// Creates an array from a result set
336
+	// If $Key is set, use the $Key column in the result set as the array key
337
+	// Otherwise, use an integer
338
+	function to_array($Key = false, $Type = MYSQLI_BOTH, $Escape = true) {
339
+		$Return = array();
340
+		while ($Row = mysqli_fetch_array($this->QueryID, $Type)) {
341
+			if ($Escape !== false) {
342
+				$Row = Misc::display_array($Row, $Escape);
343
+			}
344
+			if ($Key !== false) {
345
+				$Return[$Row[$Key]] = $Row;
346
+			} else {
347
+				$Return[] = $Row;
348
+			}
349
+		}
350
+		mysqli_data_seek($this->QueryID, 0);
351
+		return $Return;
352
+	}
353
+
354
+	//  Loops through the result set, collecting the $ValField column into an array with $KeyField as keys
355
+	function to_pair($KeyField, $ValField, $Escape = true) {
356
+		$Return = array();
357
+		while ($Row = mysqli_fetch_array($this->QueryID)) {
358
+			if ($Escape) {
359
+				$Key = display_str($Row[$KeyField]);
360
+				$Val = display_str($Row[$ValField]);
361
+			} else {
362
+				$Key = $Row[$KeyField];
363
+				$Val = $Row[$ValField];
364
+			}
365
+			$Return[$Key] = $Val;
366
+		}
367
+		mysqli_data_seek($this->QueryID, 0);
368
+		return $Return;
369
+	}
370
+
371
+	//  Loops through the result set, collecting the $Key column into an array
372
+	function collect($Key, $Escape = true) {
373
+		$Return = array();
374
+		while ($Row = mysqli_fetch_array($this->QueryID)) {
375
+			$Return[] = $Escape ? display_str($Row[$Key]) : $Row[$Key];
376
+		}
377
+		mysqli_data_seek($this->QueryID, 0);
378
+		return $Return;
379
+	}
380
+
381
+	function set_query_id(&$ResultSet) {
382
+		$this->QueryID = $ResultSet;
383
+		$this->Row = 0;
384
+	}
385
+
386
+	function get_query_id() {
387
+		return $this->QueryID;
388
+	}
389
+
390
+	function beginning() {
391
+		mysqli_data_seek($this->QueryID, 0);
392
+		$this->Row = 0;
393
+	}
394
+
395
+	/**
396
+	 * This function determines whether the last query caused warning messages
397
+	 * and stores them in $this->Queries.
398
+	 */
399
+	function warnings() {
400
+		$Warnings = array();
401
+		if (!is_bool($this->LinkID) && mysqli_warning_count($this->LinkID)) {
402
+			$e = mysqli_get_warnings($this->LinkID);
403
+			do {
404
+				if ($e->errno == 1592) {
405
+					// 1592: Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT.
406
+					continue;
407
+				}
408
+				$Warnings[] = 'Code ' . $e->errno . ': ' . display_str($e->message);
409
+			} while ($e->next());
410
+		}
411
+		$this->Queries[count($this->Queries) - 1][2] = $Warnings;
412
+	}
413
+}
414
+?>

+ 800
- 0
classes/notificationsmanager.class.php View File

1
+<?
2
+
3
+class NotificationsManager {
4
+	// Option types
5
+	const OPT_DISABLED = 0;
6
+	const OPT_POPUP = 1;
7
+	const OPT_TRADITIONAL = 2;
8
+	const OPT_PUSH = 3;
9
+	const OPT_POPUP_PUSH = 4;
10
+	const OPT_TRADITIONAL_PUSH = 5;
11
+
12
+	// Importances
13
+	const IMPORTANT = 'information';
14
+	const CRITICAL = 'error';
15
+	const WARNING = 'warning';
16
+	const INFO = 'confirmation';
17
+
18
+	public static $Importances = array(
19
+		'important' => self::IMPORTANT,
20
+		'critical' => self::CRITICAL,
21
+		'warning' => self::WARNING,
22
+		'info' => self::INFO);
23
+
24
+	// Types. These names must correspond to column names in users_notifications_settings
25
+	const NEWS = 'News';
26
+	const BLOG = 'Blog';
27
+	const STAFFBLOG = 'StaffBlog';
28
+	const STAFFPM = 'StaffPM';
29
+	const INBOX = 'Inbox';
30
+	const QUOTES = 'Quotes';
31
+	const SUBSCRIPTIONS = 'Subscriptions';
32
+	const TORRENTS = 'Torrents';
33
+	const COLLAGES = 'Collages';
34
+	const SITEALERTS = 'SiteAlerts';
35
+	const FORUMALERTS = 'ForumAlerts';
36
+	const REQUESTALERTS = 'RequestAlerts';
37
+	const COLLAGEALERTS = 'CollageAlerts';
38
+	const TORRENTALERTS = 'TorrentAlerts';
39
+	const GLOBALNOTICE = 'Global';
40
+
41
+	public static $Types = array(
42
+		'News',
43
+		'Blog',
44
+		'StaffPM',
45
+		'Inbox',
46
+		'Quotes',
47
+		'Subscriptions',
48
+		'Torrents',
49
+		'Collages',
50
+		'SiteAlerts',
51
+		'ForumAlerts',
52
+		'RequestAlerts',
53
+		'CollageAlerts',
54
+		'TorrentAlerts');
55
+
56
+	private $UserID;
57
+	private $Notifications;
58
+	private $Settings;
59
+	private $Skipped;
60
+
61
+	function __construct($UserID, $Skip = array(), $Load = true, $AutoSkip = true) {
62
+		$this->UserID = $UserID;
63
+		$this->Notifications = array();
64
+		$this->Settings = self::get_settings($UserID);
65
+		$this->Skipped = $Skip;
66
+		if ($AutoSkip) {
67
+			foreach ($this->Settings as $Key => $Value) {
68
+				// Skip disabled and traditional settings
69
+				if ($Value == self::OPT_DISABLED || $this->is_traditional($Key)) {
70
+					$this->Skipped[$Key] = true;
71
+				}
72
+			}
73
+		}
74
+		if ($Load) {
75
+			$this->load_global_notification();
76
+			if (!isset($this->Skipped[self::NEWS])) {
77
+				$this->load_news();
78
+			}
79
+			if (!isset($this->Skipped[self::BLOG])) {
80
+				$this->load_blog();
81
+			}
82
+			// if (!isset($this->Skipped[self::STAFFBLOG])) {
83
+			// 	$this->load_staff_blog();
84
+			// }
85
+			if (!isset($this->Skipped[self::STAFFPM])) {
86
+				$this->load_staff_pms();
87
+			}
88
+			if (!isset($this->Skipped[self::INBOX])) {
89
+				$this->load_inbox();
90
+			}
91
+			if (!isset($this->Skipped[self::TORRENTS])) {
92
+				$this->load_torrent_notifications();
93
+			}
94
+			if (!isset($this->Skipped[self::COLLAGES])) {
95
+				$this->load_collage_subscriptions();
96
+			}
97
+			if (!isset($this->Skipped[self::QUOTES])) {
98
+				$this->load_quote_notifications();
99
+			}
100
+			if (!isset($this->Skipped[self::SUBSCRIPTIONS])) {
101
+				$this->load_subscriptions();
102
+			}
103
+			// $this->load_one_reads(); // The code that sets these notices is commented out.
104
+		}
105
+	}
106
+
107
+	public function get_notifications() {
108
+		return $this->Notifications;
109
+	}
110
+
111
+	public function clear_notifications_array() {
112
+		unset($this->Notifications);
113
+		$this->Notifications = array();
114
+	}
115
+
116
+	private function create_notification($Type, $ID, $Message, $URL, $Importance) {
117
+		$this->Notifications[$Type] = array(
118
+			'id' => (int)$ID,
119
+			'message' => $Message,
120
+			'url' => $URL,
121
+			'importance' => $Importance);
122
+	}
123
+
124
+	public static function notify_user($UserID, $Type, $Message, $URL, $Importance) {
125
+		self::notify_users(array($UserID), $Type, $Message, $URL, $Importance);
126
+	}
127
+
128
+	public static function notify_users($UserIDs, $Type, $Message, $URL, $Importance) {
129
+		/**
130
+		if (!isset($Importance)) {
131
+			$Importance = self::INFO;
132
+		}
133
+		$Type = db_string($Type);
134
+		if (!empty($UserIDs)) {
135
+			$UserIDs = implode(',', $UserIDs);
136
+			$QueryID = G::$DB->get_query_id();
137
+			G::$DB->query("
138
+				SELECT UserID
139
+				FROM users_notifications_settings
140
+				WHERE $Type != 0
141
+					AND UserID IN ($UserIDs)");
142
+			$UserIDs = array();
143
+			while (list($ID) = G::$DB->next_record()) {
144
+				$UserIDs[] = $ID;
145
+			}
146
+			G::$DB->set_query_id($QueryID);
147
+			foreach ($UserIDs as $UserID) {
148
+				$OneReads = G::$Cache->get_value("notifications_one_reads_$UserID");
149
+				if (!$OneReads) {
150
+					$OneReads = array();
151
+				}
152
+				array_unshift($OneReads, $this->create_notification($OneReads, "oneread_" . uniqid(), null, $Message, $URL, $Importance));
153
+				$OneReads = array_filter($OneReads);
154
+				G::$Cache->cache_value("notifications_one_reads_$UserID", $OneReads, 0);
155
+			}
156
+		}
157
+		**/
158
+	}
159
+
160
+	public static function get_notification_enabled_users($Type, $UserID) {
161
+		$Type = db_string($Type);
162
+		$UserWhere = '';
163
+		if (isset($UserID)) {
164
+			$UserID = (int)$UserID;
165
+			$UserWhere = " AND UserID = '$UserID'";
166
+		}
167
+		$QueryID = G::$DB->get_query_id();
168
+		G::$DB->query("
169
+			SELECT UserID
170
+			FROM users_notifications_settings
171
+			WHERE $Type != 0
172
+				$UserWhere");
173
+		$IDs = array();
174
+		while (list($ID) = G::$DB->next_record()) {
175
+			$IDs[] = $ID;
176
+		}
177
+		G::$DB->set_query_id($QueryID);
178
+		return $IDs;
179
+	}
180
+
181
+	public function load_one_reads() {
182
+		$OneReads = G::$Cache->get_value('notifications_one_reads_' . G::$LoggedUser['ID']);
183
+		if (is_array($OneReads)) {
184
+			$this->Notifications = $this->Notifications + $OneReads;
185
+		}
186
+	}
187
+
188
+	public static function clear_one_read($ID) {
189
+		$OneReads = G::$Cache->get_value('notifications_one_reads_' . G::$LoggedUser['ID']);
190
+		if ($OneReads) {
191
+			unset($OneReads[$ID]);
192
+			if (count($OneReads) > 0) {
193
+				G::$Cache->cache_value('notifications_one_reads_' . G::$LoggedUser['ID'], $OneReads, 0);
194
+			} else {
195
+				G::$Cache->delete_value('notifications_one_reads_' . G::$LoggedUser['ID']);
196
+			}
197
+		}
198
+
199
+	}
200
+
201
+	public function load_global_notification() {
202
+		$GlobalNotification = G::$Cache->get_value('global_notification');
203
+		if ($GlobalNotification) {
204
+			$Read = G::$Cache->get_value('user_read_global_' . G::$LoggedUser['ID']);
205
+			if (!$Read) {
206
+				$this->create_notification(self::GLOBALNOTICE, 0,  $GlobalNotification['Message'], $GlobalNotification['URL'], $GlobalNotification['Importance']);
207
+			}
208
+		}
209
+	}
210
+
211
+	public static function get_global_notification() {
212
+		return G::$Cache->get_value('global_notification');
213
+	}
214
+
215
+	public static function set_global_notification($Message, $URL, $Importance, $Expiration) {
216
+		if (empty($Message) || empty($Expiration)) {
217
+			error('Error setting notification');
218
+		}
219
+		G::$Cache->cache_value('global_notification', array("Message" => $Message, "URL" => $URL, "Importance" => $Importance, "Expiration" => $Expiration), $Expiration);
220
+	}
221
+
222
+	public static function delete_global_notification() {
223
+		G::$Cache->delete_value('global_notification');
224
+	}
225
+
226
+	public static function clear_global_notification() {
227
+		$GlobalNotification = G::$Cache->get_value('global_notification');
228
+		if ($GlobalNotification) {
229
+			// This is some trickery
230
+			// since we can't know which users have the read cache key set
231
+			// we set the expiration time of their cache key to that of the length of the notification
232
+			// this gaurantees that their cache key will expire after the notification expires
233
+			G::$Cache->cache_value('user_read_global_' . G::$LoggedUser['ID'], true, $GlobalNotification['Expiration']);
234
+		}
235
+	}
236
+
237
+	public function load_news() {
238
+		$MyNews = G::$LoggedUser['LastReadNews'];
239
+		$CurrentNews = G::$Cache->get_value('news_latest_id');
240
+		$Title = G::$Cache->get_value('news_latest_title');
241
+		if ($CurrentNews === false || $Title === false) {
242
+			$QueryID = G::$DB->get_query_id();
243
+			G::$DB->query('
244
+				SELECT ID, Title
245
+				FROM news
246
+				ORDER BY Time DESC
247
+				LIMIT 1');
248
+			if (G::$DB->has_results()) {
249
+				list($CurrentNews, $Title) = G::$DB->next_record();
250
+			} else {
251
+				$CurrentNews = -1;
252
+			}
253
+			G::$DB->set_query_id($QueryID);
254
+			G::$Cache->cache_value('news_latest_id', $CurrentNews, 0);
255
+			G::$Cache->cache_value('news_latest_title', $Title, 0);
256
+		}
257
+		if ($MyNews < $CurrentNews) {
258
+			$this->create_notification(self::NEWS, $CurrentNews, "Announcement: $Title", "index.php#news$CurrentNews", self::IMPORTANT);
259
+		}
260
+	}
261
+
262
+	public function load_blog() {
263
+		$MyBlog = G::$LoggedUser['LastReadBlog'];
264
+		$CurrentBlog = G::$Cache->get_value('blog_latest_id');
265
+		$Title = G::$Cache->get_value('blog_latest_title');
266
+		if ($CurrentBlog === false) {
267
+			$QueryID = G::$DB->get_query_id();
268
+			G::$DB->query('
269
+				SELECT ID, Title
270
+				FROM blog
271
+				WHERE Important = 1
272
+				ORDER BY Time DESC
273
+				LIMIT 1');
274
+			if (G::$DB->has_results()) {
275
+				list($CurrentBlog, $Title) = G::$DB->next_record();
276
+			} else {
277
+				$CurrentBlog = -1;
278
+			}
279
+			G::$DB->set_query_id($QueryID);
280
+			G::$Cache->cache_value('blog_latest_id', $CurrentBlog, 0);
281
+			G::$Cache->cache_value('blog_latest_title', $Title, 0);
282
+		}
283
+		if ($MyBlog < $CurrentBlog) {
284
+			$this->create_notification(self::BLOG, $CurrentBlog, "Blog: $Title", "blog.php#blog$CurrentBlog", self::IMPORTANT);
285
+		}
286
+	}
287
+
288
+	public function load_staff_blog() {
289
+		if (check_perms('users_mod')) {
290
+			global $SBlogReadTime, $LatestSBlogTime;
291
+			if (!$SBlogReadTime && ($SBlogReadTime = G::$Cache->get_value('staff_blog_read_' . G::$LoggedUser['ID'])) === false) {
292
+				$QueryID = G::$DB->get_query_id();
293
+				G::$DB->query("
294
+					SELECT Time
295
+					FROM staff_blog_visits
296
+					WHERE UserID = " . G::$LoggedUser['ID']);
297
+				if (list($SBlogReadTime) = G::$DB->next_record()) {
298
+					$SBlogReadTime = strtotime($SBlogReadTime);
299
+				} else {
300
+					$SBlogReadTime = 0;
301
+				}
302
+				G::$DB->set_query_id($QueryID);
303
+				G::$Cache->cache_value('staff_blog_read_' . G::$LoggedUser['ID'], $SBlogReadTime, 1209600);
304
+			}
305
+			if (!$LatestSBlogTime && ($LatestSBlogTime = G::$Cache->get_value('staff_blog_latest_time')) === false) {
306
+				$QueryID = G::$DB->get_query_id();
307
+				G::$DB->query('
308
+					SELECT MAX(Time)
309
+					FROM staff_blog');
310
+				if (list($LatestSBlogTime) = G::$DB->next_record()) {
311
+					$LatestSBlogTime = strtotime($LatestSBlogTime);
312
+				} else {
313
+					$LatestSBlogTime = 0;
314
+				}
315
+				G::$DB->set_query_id($QueryID);
316
+				G::$Cache->cache_value('staff_blog_latest_time', $LatestSBlogTime, 1209600);
317
+			}
318
+			if ($SBlogReadTime < $LatestSBlogTime) {
319
+				$this->create_notification(self::STAFFBLOG, 0, 'New Staff Blog Post!', 'staffblog.php', self::IMPORTANT);
320
+			}
321
+		}
322
+	}
323
+
324
+	public function load_staff_pms() {
325
+		$NewStaffPMs = G::$Cache->get_value('staff_pm_new_' . G::$LoggedUser['ID']);
326
+		if ($NewStaffPMs === false) {
327
+			$QueryID = G::$DB->get_query_id();
328
+			G::$DB->query("
329
+				SELECT COUNT(ID)
330
+				FROM staff_pm_conversations
331
+				WHERE UserID = '" . G::$LoggedUser['ID'] . "'
332
+					AND Unread = '1'");
333
+			list($NewStaffPMs) = G::$DB->next_record();
334
+			G::$DB->set_query_id($QueryID);
335
+			G::$Cache->cache_value('staff_pm_new_' . G::$LoggedUser['ID'], $NewStaffPMs, 0);
336
+		}
337
+
338
+		if ($NewStaffPMs > 0) {
339
+			$Title = 'You have ' . ($NewStaffPMs == 1 ? 'a' : $NewStaffPMs) . ' new Staff PM' . ($NewStaffPMs > 1 ? 's' : '');
340
+			$this->create_notification(self::STAFFPM, 0, $Title, 'staffpm.php', self::INFO);
341
+		}
342
+	}
343
+
344
+	public function load_inbox() {
345
+		$NewMessages = G::$Cache->get_value('inbox_new_' . G::$LoggedUser['ID']);
346
+		if ($NewMessages === false) {
347
+			$QueryID = G::$DB->get_query_id();
348
+			G::$DB->query("
349
+				SELECT COUNT(UnRead)
350
+				FROM pm_conversations_users
351
+				WHERE UserID = '" . G::$LoggedUser['ID'] . "'
352
+					AND UnRead = '1'
353
+					AND InInbox = '1'");
354
+			list($NewMessages) = G::$DB->next_record();
355
+			G::$DB->set_query_id($QueryID);
356
+			G::$Cache->cache_value('inbox_new_' . G::$LoggedUser['ID'], $NewMessages, 0);
357
+		}
358
+
359
+		if ($NewMessages > 0) {
360
+			$Title = 'You have ' . ($NewMessages == 1 ? 'a' : $NewMessages) . ' new message' . ($NewMessages > 1 ? 's' : '');
361
+			$this->create_notification(self::INBOX, 0, $Title, Inbox::get_inbox_link(), self::INFO);
362
+		}
363
+	}
364
+
365
+	public function load_torrent_notifications() {
366
+		if (check_perms('site_torrents_notify')) {
367
+			$NewNotifications = G::$Cache->get_value('notifications_new_' . G::$LoggedUser['ID']);
368
+			if ($NewNotifications === false) {
369
+				$QueryID = G::$DB->get_query_id();
370
+				G::$DB->query("
371
+					SELECT COUNT(UserID)
372
+					FROM users_notify_torrents
373
+					WHERE UserID = ' " . G::$LoggedUser['ID'] . "'
374
+						AND UnRead = '1'");
375
+				list($NewNotifications) = G::$DB->next_record();
376
+				G::$DB->set_query_id($QueryID);
377
+				G::$Cache->cache_value('notifications_new_' . G::$LoggedUser['ID'], $NewNotifications, 0);
378
+			}
379
+		}
380
+		if (isset($NewNotifications) && $NewNotifications > 0) {
381
+			$Title = 'You have ' . ($NewNotifications == 1 ? 'a' : $NewNotifications) . ' new torrent notification' . ($NewNotifications > 1 ? 's' : '');
382
+			$this->create_notification(self::TORRENTS, 0, $Title, 'torrents.php?action=notify', self::INFO);
383
+		}
384
+	}
385
+
386
+	public function load_collage_subscriptions() {
387
+		if (check_perms('site_collages_subscribe')) {
388
+			$NewCollages = G::$Cache->get_value('collage_subs_user_new_' . G::$LoggedUser['ID']);
389
+			if ($NewCollages === false) {
390
+					$QueryID = G::$DB->get_query_id();
391
+					G::$DB->query("
392
+						SELECT COUNT(DISTINCT s.CollageID)
393
+						FROM users_collage_subs AS s
394
+							JOIN collages AS c ON s.CollageID = c.ID
395
+							JOIN collages_torrents AS ct ON ct.CollageID = c.ID
396
+						WHERE s.UserID = " . G::$LoggedUser['ID'] . "
397
+							AND ct.AddedOn > s.LastVisit
398
+							AND c.Deleted = '0'");
399
+					list($NewCollages) = G::$DB->next_record();
400
+					G::$DB->set_query_id($QueryID);
401
+					G::$Cache->cache_value('collage_subs_user_new_' . G::$LoggedUser['ID'], $NewCollages, 0);
402
+			}
403
+			if ($NewCollages > 0) {
404
+				$Title = 'You have ' . ($NewCollages == 1 ? 'a' : $NewCollages) . ' new collage update' . ($NewCollages > 1 ? 's' : '');
405
+				$this->create_notification(self::COLLAGES, 0, $Title, 'userhistory.php?action=subscribed_collages', self::INFO);
406
+			}
407
+		}
408
+	}
409
+
410
+	public function load_quote_notifications() {
411
+		if (isset(G::$LoggedUser['NotifyOnQuote']) && G::$LoggedUser['NotifyOnQuote']) {
412
+			$QuoteNotificationsCount = Subscriptions::has_new_quote_notifications();
413
+			if ($QuoteNotificationsCount > 0) {
414
+				$Title = 'New quote' . ($QuoteNotificationsCount > 1 ? 's' : '');
415
+				$this->create_notification(self::QUOTES, 0, $Title, 'userhistory.php?action=quote_notifications', self::INFO);
416
+			}
417
+		}
418
+	}
419
+
420
+	public function load_subscriptions() {
421
+		$SubscriptionsCount = Subscriptions::has_new_subscriptions();
422
+		if ($SubscriptionsCount > 0) {
423
+			$Title = 'New subscription' . ($SubscriptionsCount > 1 ? 's' : '');
424
+			$this->create_notification(self::SUBSCRIPTIONS, 0, $Title, 'userhistory.php?action=subscriptions', self::INFO);
425
+		}
426
+	}
427
+
428
+	public static function clear_news($News) {
429
+		$QueryID = G::$DB->get_query_id();
430
+		if (!$News) {
431
+			if (!$News = G::$Cache->get_value('news')) {
432
+				G::$DB->query('
433
+					SELECT
434
+						ID,
435
+						Title,
436
+						Body,
437
+						Time
438
+					FROM news
439
+					ORDER BY Time DESC
440
+					LIMIT 1');
441
+				$News = G::$DB->to_array(false, MYSQLI_NUM, false);
442
+				G::$Cache->cache_value('news_latest_id', $News[0][0], 0);
443
+			}
444
+		}
445
+
446
+		if (G::$LoggedUser['LastReadNews'] != $News[0][0]) {
447
+			G::$Cache->begin_transaction('user_info_heavy_' . G::$LoggedUser['ID']);
448
+			G::$Cache->update_row(false, array('LastReadNews' => $News[0][0]));
449
+			G::$Cache->commit_transaction(0);
450
+			G::$DB->query("
451
+				UPDATE users_info
452
+				SET LastReadNews = '".$News[0][0]."'
453
+				WHERE UserID = " . G::$LoggedUser['ID']);
454
+			G::$LoggedUser['LastReadNews'] = $News[0][0];
455
+		}
456
+		G::$DB->set_query_id($QueryID);
457
+	}
458
+
459
+	public static function clear_blog($Blog) {
460
+		$QueryID = G::$DB->get_query_id();
461
+		if (!isset($Blog) || !$Blog) {
462
+			if (!$Blog = G::$Cache->get_value('blog')) {
463
+				G::$DB->query("
464
+					SELECT
465
+						b.ID,
466
+						um.Username,
467
+						b.UserID,
468
+						b.Title,
469
+						b.Body,
470
+						b.Time,
471
+						b.ThreadID
472
+					FROM blog AS b
473
+						LEFT JOIN users_main AS um ON b.UserID = um.ID
474
+					ORDER BY Time DESC
475
+					LIMIT 1");
476
+				$Blog = G::$DB->to_array();
477
+			}
478
+		}
479
+		if (G::$LoggedUser['LastReadBlog'] < $Blog[0][0]) {
480
+			G::$Cache->begin_transaction('user_info_heavy_' . G::$LoggedUser['ID']);
481
+			G::$Cache->update_row(false, array('LastReadBlog' => $Blog[0][0]));
482
+			G::$Cache->commit_transaction(0);
483
+			G::$DB->query("
484
+				UPDATE users_info
485
+				SET LastReadBlog = '". $Blog[0][0]."'
486
+				WHERE UserID = " . G::$LoggedUser['ID']);
487
+			G::$LoggedUser['LastReadBlog'] = $Blog[0][0];
488
+		}
489
+		G::$DB->set_query_id($QueryID);
490
+	}
491
+
492
+	public static function clear_staff_pms() {
493
+		$QueryID = G::$DB->get_query_id();
494
+		G::$DB->query("
495
+			SELECT ID
496
+			FROM staff_pm_conversations
497
+			WHERE Unread = true
498
+				AND UserID = " . G::$LoggedUser['ID']);
499
+		$IDs = array();
500
+		while (list($ID) = G::$DB->next_record()) {
501
+			$IDs[] = $ID;
502
+		}
503
+		$IDs = implode(',', $IDs);
504
+		if (!empty($IDs)) {
505
+			G::$DB->query("
506
+				UPDATE staff_pm_conversations
507
+				SET Unread = false
508
+				WHERE ID IN ($IDs)");
509
+		}
510
+		G::$Cache->delete_value('staff_pm_new_' . G::$LoggedUser['ID']);
511
+		G::$DB->set_query_id($QueryID);
512
+	}
513
+
514
+	public static function clear_inbox() {
515
+		$QueryID = G::$DB->get_query_id();
516
+		G::$DB->query("
517
+			SELECT ConvID
518
+			FROM pm_conversations_users
519
+			WHERE Unread = '1'
520
+				AND UserID = " . G::$LoggedUser['ID']);
521
+		$IDs = array();
522
+		while (list($ID) = G::$DB->next_record()) {
523
+			$IDs[] = $ID;
524
+		}
525
+		$IDs = implode(',', $IDs);
526
+		if (!empty($IDs)) {
527
+			G::$DB->query("
528
+				UPDATE pm_conversations_users
529
+				SET Unread = '0'
530
+				WHERE ConvID IN ($IDs)
531
+					AND UserID = " . G::$LoggedUser['ID']);
532
+		}
533
+		G::$Cache->delete_value('inbox_new_' . G::$LoggedUser['ID']);
534
+		G::$DB->set_query_id($QueryID);
535
+	}
536
+
537
+	public static function clear_torrents() {
538
+		$QueryID = G::$DB->get_query_id();
539
+		G::$DB->query("
540
+			SELECT TorrentID
541
+			FROM users_notify_torrents
542
+			WHERE UserID = ' " . G::$LoggedUser['ID'] . "'
543
+				AND UnRead = '1'");
544
+		$IDs = array();
545
+		while (list($ID) = G::$DB->next_record()) {
546
+			$IDs[] = $ID;
547
+		}
548
+		$IDs = implode(',', $IDs);
549
+		if (!empty($IDs)) {
550
+			G::$DB->query("
551
+				UPDATE users_notify_torrents
552
+				SET Unread = '0'
553
+				WHERE TorrentID IN ($IDs)
554
+					AND UserID = " . G::$LoggedUser['ID']);
555
+		}
556
+		G::$Cache->delete_value('notifications_new_' . G::$LoggedUser['ID']);
557
+		G::$DB->set_query_id($QueryID);
558
+	}
559
+
560
+	public static function clear_collages() {
561
+		$QueryID = G::$DB->get_query_id();
562
+		G::$DB->query("
563
+			UPDATE users_collage_subs
564
+			SET LastVisit = NOW()
565
+			WHERE UserID = " . G::$LoggedUser['ID']);
566
+		G::$Cache->delete_value('collage_subs_user_new_' . G::$LoggedUser['ID']);
567
+		G::$DB->set_query_id($QueryID);
568
+	}
569
+
570
+	public static function clear_quotes() {
571
+		$QueryID = G::$DB->get_query_id();
572
+		G::$DB->query("
573
+			UPDATE users_notify_quoted
574
+			SET UnRead = '0'
575
+			WHERE UserID = " . G::$LoggedUser['ID']);
576
+		G::$Cache->delete_value('notify_quoted_' . G::$LoggedUser['ID']);
577
+		G::$DB->set_query_id($QueryID);
578
+	}
579
+
580
+	public static function clear_subscriptions() {
581
+		$QueryID = G::$DB->get_query_id();
582
+		if (($UserSubscriptions = G::$Cache->get_value('subscriptions_user_' . G::$LoggedUser['ID'])) === false) {
583
+			G::$DB->query("
584
+				SELECT TopicID
585
+				FROM users_subscriptions
586
+				WHERE UserID = " . G::$LoggedUser['ID']);
587
+			if ($UserSubscriptions = G::$DB->collect(0)) {
588
+				G::$Cache->cache_value('subscriptions_user_' . G::$LoggedUser['ID'], $UserSubscriptions, 0);
589
+			}
590
+		}
591
+		if (!empty($UserSubscriptions)) {
592
+			G::$DB->query("
593
+				INSERT INTO forums_last_read_topics (UserID, TopicID, PostID)
594
+					SELECT '" . G::$LoggedUser['ID'] . "', ID, LastPostID
595
+					FROM forums_topics
596
+					WHERE ID IN (".implode(',', $UserSubscriptions).')
597
+				ON DUPLICATE KEY UPDATE
598
+					PostID = LastPostID');
599
+		}
600
+		G::$Cache->delete_value('subscriptions_user_new_' . G::$LoggedUser['ID']);
601
+		G::$DB->set_query_id($QueryID);
602
+	}
603
+
604
+/*
605
+	// TODO: Figure out what these functions are supposed to do and fix them
606
+	public static function send_notification($UserID, $ID, $Type, $Message, $URL, $Importance = 'alert', $AutoExpire = false) {
607
+		$Notifications = G::$Cache->get_value("user_cache_notifications_$UserID");
608
+		if (empty($Notifications)) {
609
+			$Notifications = array();
610
+		}
611
+		array_unshift($Notifications, $this->create_notification($Type, $ID, $Message, $URL, $Importance, $AutoExpire));
612
+		G::$Cache->cache_value("user_cache_notifications_$UserID", $Notifications, 0);
613
+	}
614
+
615
+	public static function clear_notification($UserID, $Index) {
616
+		$Notifications = G::$Cache->get_value("user_cache_notifications_$UserID");
617
+		if (count($Notifications)) {
618
+			unset($Notifications[$Index]);
619
+			$Notifications = array_values($Notifications);
620
+			G::$Cache->cache_value("user_cache_notifications_$UserID", $Notifications, 0);
621
+		}
622
+	}
623
+*/
624
+
625
+	public static function get_settings($UserID) {
626
+		$Results = G::$Cache->get_value("users_notifications_settings_$UserID");
627
+		if (!$Results) {
628
+			$QueryID = G::$DB->get_query_id();
629
+			G::$DB->query("
630
+				SELECT *
631
+				FROM users_notifications_settings AS n
632
+				LEFT JOIN users_push_notifications AS p
633
+				ON p.UserID = n.UserID
634
+				WHERE n.UserID = '$UserID'");
635
+			$Results = G::$DB->next_record(MYSQLI_ASSOC, false);
636
+			G::$DB->set_query_id($QueryID);
637
+			G::$Cache->cache_value("users_notifications_settings_$UserID", $Results, 0);
638
+		}
639
+		return $Results;
640
+	}
641
+
642
+	public static function save_settings($UserID, $Settings) {
643
+		if (!is_array($Settings)) {
644
+			// A little cheat technique, gets all keys in the $_POST array starting with 'notifications_'
645
+			$Settings = array_intersect_key($_POST, array_flip(preg_grep('/^notifications_/', array_keys($_POST))));
646
+		}
647
+		$Update = array();
648
+		foreach (self::$Types as $Type) {
649
+			$Popup = array_key_exists("notifications_{$Type}_popup", $Settings);
650
+			$Traditional = array_key_exists("notifications_{$Type}_traditional", $Settings);
651
+			$Push = array_key_exists("notifications_{$Type}_push", $Settings);
652
+			$Result = self::OPT_DISABLED;
653
+			if ($Popup) {
654
+				$Result = $Push ? self::OPT_POPUP_PUSH : self::OPT_POPUP;
655
+			} elseif ($Traditional) {
656
+				$Result = $Push ? self::OPT_TRADITIONAL_PUSH : self::OPT_TRADITIONAL;
657
+			} elseif ($Push) {
658
+				$Result = self::OPT_PUSH;
659
+			}
660
+			$Update[] = "$Type = $Result";
661
+		}
662
+		$Update = implode(',', $Update);
663
+
664
+		$QueryID = G::$DB->get_query_id();
665
+		G::$DB->query("
666
+			UPDATE users_notifications_settings
667
+			SET $Update
668
+			WHERE UserID = '$UserID'");
669
+
670
+		$PushService = (int) $_POST['pushservice'];
671
+		$PushOptionsArray = array("PushKey" => $_POST['pushkey']);
672
+		if ($PushService === 6) { //pushbullet
673
+			$PushOptionsArray['PushDevice'] = $_POST['pushdevice'];
674
+		}
675
+		$PushOptions = db_string(serialize($PushOptionsArray));
676
+
677
+		if ($PushService != 0) {
678
+			G::$DB->query("
679
+					INSERT INTO users_push_notifications
680
+						(UserID, PushService, PushOptions)
681
+					VALUES
682
+						('$UserID', '$PushService', '$PushOptions')
683
+					ON DUPLICATE KEY UPDATE
684
+						PushService = '$PushService',
685
+						PushOptions = '$PushOptions'");
686
+		} else {
687
+			G::$DB->query("UPDATE users_push_notifications SET PushService = 0 WHERE UserID = '$UserID'");
688
+		}
689
+
690
+		G::$DB->set_query_id($QueryID);
691
+		G::$Cache->delete_value("users_notifications_settings_$UserID");
692
+	}
693
+
694
+	public function is_traditional($Type) {
695
+		return $this->Settings[$Type] == self::OPT_TRADITIONAL || $this->Settings[$Type] == self::OPT_TRADITIONAL_PUSH;
696
+	}
697
+
698
+	public function is_skipped($Type) {
699
+		return isset($this->Skipped[$Type]);
700
+	}
701
+
702
+	public function use_noty() {
703
+		return in_array(self::OPT_POPUP, $this->Settings) || in_array(self::OPT_POPUP_PUSH, $this->Settings);
704
+	}
705
+
706
+	/**
707
+	 * Send a push notification to a user
708
+	 *
709
+	 * @param array $UserIDs integer or array of integers of UserIDs to push
710
+	 * @param string $Title the title to be displayed in the push
711
+	 * @param string $Body the body of the push
712
+	 * @param string $URL url for the push notification to contain
713
+	 * @param string $Type what sort of push is it? PM, Rippy, News, etc
714
+	 */
715
+	public static function send_push($UserIDs, $Title, $Body, $URL = '', $Type = self::GLOBALNOTICE) {
716
+		if (!is_array($UserIDs)) {
717
+			$UserIDs = array($UserIDs);
718
+		}
719
+		foreach($UserIDs as $UserID) {
720
+			$UserID = (int) $UserID;
721
+			$QueryID = G::$DB->get_query_id();
722
+			$SQL = "
723
+				SELECT
724
+					p.PushService, p.PushOptions
725
+				FROM users_notifications_settings AS n
726
+					JOIN users_push_notifications AS p ON n.UserID = p.UserID
727
+				WHERE n.UserID = '$UserID'
728
+				AND p.PushService != 0";
729
+			if ($Type != self::GLOBALNOTICE) {
730
+				$SQL .= " AND n.$Type IN (" . self::OPT_PUSH . "," . self::OPT_POPUP_PUSH . "," . self::OPT_TRADITIONAL_PUSH . ")";
731
+			}
732
+			G::$DB->query($SQL);
733
+
734
+			if (G::$DB->has_results()) {
735
+				list($PushService, $PushOptions) = G::$DB->next_record(MYSQLI_NUM, false);
736
+				$PushOptions = unserialize($PushOptions);
737
+				switch ($PushService) {
738
+					case '1':
739
+						$Service = "NMA";
740
+						break;
741
+					case '2':
742
+						$Service = "Prowl";
743
+						break;
744
+					// Case 3 is missing because notifo is dead.
745
+					case '4':
746
+						$Service = "Toasty";
747
+						break;
748
+					case '5':
749
+						$Service = "Pushover";
750
+						break;
751
+					case '6':
752
+						$Service = "PushBullet";
753
+						break;
754
+					default:
755
+						break;
756
+					}
757
+					if (!empty($Service) && !empty($PushOptions['PushKey'])) {
758
+						$Options = array("service" => strtolower($Service),
759
+										"user" => array("key" => $PushOptions['PushKey']),
760
+										"message" => array("title" => $Title, "body" => $Body, "url" => $URL));
761
+
762
+						if ($Service === 'PushBullet') {
763
+							$Options["user"]["device"] = $PushOptions['PushDevice'];
764
+
765
+						}
766
+
767
+						$JSON = json_encode($Options);
768
+						G::$DB->query("
769
+							INSERT INTO push_notifications_usage
770
+								(PushService, TimesUsed)
771
+							VALUES
772
+								('$Service', 1)
773
+							ON DUPLICATE KEY UPDATE
774
+								TimesUsed = TimesUsed + 1");
775
+
776
+						$PushServerSocket = fsockopen("127.0.0.1", 6789);
777
+						fwrite($PushServerSocket, $JSON);
778
+						fclose($PushServerSocket);
779
+					}
780
+			}
781
+			G::$DB->set_query_id($QueryID);
782
+		}
783
+	}
784
+
785
+	/**
786
+	 * Gets users who have push notifications enabled
787
+	 *
788
+	 */
789
+	public static function get_push_enabled_users() {
790
+		$QueryID = G::$DB->get_query_id();
791
+		G::$DB->query("
792
+			SELECT UserID
793
+			FROM users_push_notifications
794
+			WHERE PushService != 0
795
+				AND UserID != '" . G::$LoggedUser['ID']. "'");
796
+		$PushUsers = G::$DB->collect("UserID");
797
+		G::$DB->set_query_id($QueryID);
798
+		return $PushUsers;
799
+	}
800
+}

+ 156
- 0
classes/notificationsmanagerview.class.php View File

1
+<?
2
+
3
+class NotificationsManagerView {
4
+	private static $Settings;
5
+
6
+	public static function load_js() {
7
+		$JSIncludes = array(
8
+			'noty/noty.js',
9
+			'noty/layouts/bottomRight.js',
10
+			'noty/themes/default.js',
11
+			'user_notifications.js');
12
+		foreach ($JSIncludes as $JSInclude) {
13
+			$Path = STATIC_SERVER."functions/$JSInclude";
14
+?>
15
+	<script src="<?=$Path?>?v=<?=filemtime(SERVER_ROOT."/$Path")?>" type="text/javascript"></script>
16
+<?
17
+		}
18
+	}
19
+
20
+	private static function render_push_settings() {
21
+		$PushService = self::$Settings['PushService'];
22
+		$PushOptions = unserialize(self::$Settings['PushOptions']);
23
+		if (empty($PushOptions['PushDevice'])) {
24
+			$PushOptions['PushDevice'] = '';
25
+		}
26
+		?>
27
+		<tr>
28
+			<td class="label"><strong>Push notifications</strong></td>
29
+			<td>
30
+				<select name="pushservice" id="pushservice">
31
+					<option value="0"<? if (empty($PushService)) { ?> selected="selected"<? } ?>>Disable push notifications</option>
32
+					<option value="1"<? if ($PushService == 1) { ?> selected="selected"<? } ?>>Notify My Android</option>
33
+					<option value="2"<? if ($PushService == 2) { ?> selected="selected"<? } ?>>Prowl</option>
34
+<!--						No option 3, notifo died. -->
35
+					<option value="4"<? if ($PushService == 4) { ?> selected="selected"<? } ?>>Super Toasty</option>
36
+					<option value="5"<? if ($PushService == 5) { ?> selected="selected"<? } ?>>Pushover</option>
37
+					<option value="6"<? if ($PushService == 6) { ?> selected="selected"<? } ?>>PushBullet</option>
38
+				</select>
39
+				<div id="pushsettings" style="display: none;">
40
+					<label id="pushservice_title" for="pushkey">API key</label>
41
+					<input type="text" size="50" name="pushkey" id="pushkey" value="<?=display_str($PushOptions['PushKey'])?>" />
42
+					<label class="pushdeviceid" id="pushservice_device" for="pushdevice">Device ID</label>
43
+					<select class="pushdeviceid" name="pushdevice" id="pushdevice">
44
+						<option value="<?= display_str($PushOptions['PushDevice'])?>" selected="selected"><?= display_str($PushOptions['PushDevice'])?></option>
45
+					</select>
46
+					<br />
47
+					<a href="user.php?action=take_push&amp;push=1&amp;userid=<?=G::$LoggedUser['ID']?>&amp;auth=<?=G::$LoggedUser['AuthKey']?>" class="brackets">Test push</a>
48
+					<a href="wiki.php?action=article&amp;id=1017" class="brackets">View wiki guide</a>
49
+				</div>
50
+			</td>
51
+		</tr>
52
+<?	}
53
+
54
+	public static function render_settings($Settings) {
55
+		self::$Settings = $Settings;
56
+		self::render_push_settings();
57
+?>
58
+		<tr>
59
+			<td class="label">
60
+				<strong>News announcements</strong>
61
+			</td>
62
+			<td>
63
+<?				self::render_checkbox(NotificationsManager::NEWS); ?>
64
+			</td>
65
+		</tr>
66
+		<tr>
67
+			<td class="label">
68
+				<strong>Blog announcements</strong>
69
+			</td>
70
+			<td>
71
+<?				self::render_checkbox(NotificationsManager::BLOG); ?>
72
+			</td>
73
+		</tr>
74
+		<tr>
75
+			<td class="label">
76
+				<strong>Inbox messages</strong>
77
+			</td>
78
+			<td>
79
+<?				self::render_checkbox(NotificationsManager::INBOX, true); ?>
80
+			</td>
81
+		</tr>
82
+		<tr>
83
+			<td class="label tooltip" title="Enabling this will give you a notification when you receive a new private message from a member of the <?=SITE_NAME?> staff.">
84
+				<strong>Staff messages</strong>
85
+			</td>
86
+			<td>
87
+<?				self::render_checkbox(NotificationsManager::STAFFPM, false, false); ?>
88
+			</td>
89
+		</tr>
90
+		<tr>
91
+			<td class="label">
92
+				<strong>Thread subscriptions</strong>
93
+			</td>
94
+			<td>
95
+<?				self::render_checkbox(NotificationsManager::SUBSCRIPTIONS, false, false); ?>
96
+			</td>
97
+		</tr>
98
+		<tr>
99
+			<td class="label tooltip" title="Enabling this will give you a notification whenever someone quotes you in the forums.">
100
+				<strong>Quote notifications</strong>
101
+			</td>
102
+			<td>
103
+<?				self::render_checkbox(NotificationsManager::QUOTES); ?>
104
+			</td>
105
+		</tr>
106
+<? 		if (check_perms('site_torrents_notify')) { ?>
107
+			<tr>
108
+				<td class="label tooltip" title="Enabling this will give you a notification when the torrent notification filters you have established are triggered.">
109
+					<strong>Torrent notifications</strong>
110
+				</td>
111
+				<td>
112
+<?					self::render_checkbox(NotificationsManager::TORRENTS, true, false); ?>
113
+				</td>
114
+			</tr>
115
+<?		} ?>
116
+
117
+		<tr>
118
+			<td class="label tooltip" title="Enabling this will give you a notification when a torrent is added to a collage you are subscribed to.">
119
+				<strong>Collage subscriptions</strong>
120
+			</td>
121
+			<td>
122
+<?				self::render_checkbox(NotificationsManager::COLLAGES. false, false); ?>
123
+			</td>
124
+		</tr>
125
+<?	}
126
+
127
+	private static function render_checkbox($Name, $Traditional = false, $Push = true) {
128
+		$Checked = self::$Settings[$Name];
129
+		$PopupChecked = $Checked == NotificationsManager::OPT_POPUP || $Checked == NotificationsManager::OPT_POPUP_PUSH || !isset($Checked) ? ' checked="checked"' : '';
130
+		$TraditionalChecked = $Checked == NotificationsManager::OPT_TRADITIONAL || $Checked == NotificationsManager::OPT_TRADITIONAL_PUSH ? ' checked="checked"' : '';
131
+		$PushChecked = $Checked == NotificationsManager::OPT_TRADITIONAL_PUSH || $Checked == NotificationsManager::OPT_POPUP_PUSH || $Checked == NotificationsManager::OPT_PUSH ? ' checked="checked"' : '';
132
+
133
+?>
134
+		<label>
135
+			<input type="checkbox" name="notifications_<?=$Name?>_popup" id="notifications_<?=$Name?>_popup"<?=$PopupChecked?> />
136
+			Pop-up
137
+		</label>
138
+<?		if ($Traditional) { ?>
139
+		<label>
140
+			<input type="checkbox" name="notifications_<?=$Name?>_traditional" id="notifications_<?=$Name?>_traditional"<?=$TraditionalChecked?> />
141
+			Traditional
142
+		</label>
143
+<?		}
144
+		if ($Push) { ?>
145
+		<label>
146
+			<input type="checkbox" name="notifications_<?=$Name?>_push" id="notifications_<?=$Name?>_push"<?=$PushChecked?> />
147
+			Push
148
+		</label>
149
+<?		}
150
+	}
151
+
152
+	public static function format_traditional($Contents) {
153
+		return "<a href=\"$Contents[url]\">$Contents[message]</a>";
154
+	}
155
+
156
+}

+ 103
- 0
classes/paranoia.class.php View File

1
+<?
2
+
3
+// Note: at the time this file is loaded, check_perms is not defined. Don't
4
+// call check_paranoia in /classes/script_start.php without ensuring check_perms has been defined
5
+
6
+// The following are used throughout the site:
7
+// uploaded, ratio, downloaded: stats
8
+// lastseen: approximate time the user last used the site
9
+// uploads: the full list of the user's uploads
10
+// uploads+: just how many torrents the user has uploaded
11
+// snatched, seeding, leeching: the list of the user's snatched torrents, seeding torrents, and leeching torrents respectively
12
+// snatched+, seeding+, leeching+: the length of those lists respectively
13
+// uniquegroups, perfectflacs: the list of the user's uploads satisfying a particular criterion
14
+// uniquegroups+, perfectflacs+: the length of those lists
15
+// If "uploads+" is disallowed, so is "uploads". So if "uploads" is in the array, the user is a little paranoid, "uploads+", very paranoid.
16
+
17
+// The following are almost only used in /sections/user/user.php:
18
+// requiredratio
19
+// requestsfilled_count: the number of requests the user has filled
20
+//   requestsfilled_bounty: the bounty thus earned
21
+//   requestsfilled_list: the actual list of requests the user has filled
22
+// requestsvoted_...: similar
23
+// artistsadded: the number of artists the user has added
24
+// torrentcomments: the list of comments the user has added to torrents
25
+//   +
26
+// collages: the list of collages the user has created
27
+//   +
28
+// collagecontribs: the list of collages the user has contributed to
29
+//   +
30
+// invitedcount: the number of users this user has directly invited
31
+
32
+/**
33
+ * Return whether currently logged in user can see $Property on a user with $Paranoia, $UserClass and (optionally) $UserID
34
+ * If $Property is an array of properties, returns whether currently logged in user can see *all* $Property ...
35
+ *
36
+ * @param $Property The property to check, or an array of properties.
37
+ * @param $Paranoia The paranoia level to check against.
38
+ * @param $UserClass The user class to check against (Staff can see through paranoia of lower classed staff)
39
+ * @param $UserID Optional. The user ID of the person being viewed
40
+ * @return mixed   1 representing the user has normal access
41
+				   2 representing that the paranoia was overridden,
42
+				   false representing access denied.
43
+ */
44
+
45
+define("PARANOIA_ALLOWED", 1);
46
+define("PARANOIA_OVERRIDDEN", 2);
47
+
48
+function check_paranoia($Property, $Paranoia, $UserClass, $UserID = false) {
49
+	global $Classes;
50
+	if ($Property == false) {
51
+		return false;
52
+	}
53
+	if (!is_array($Paranoia)) {
54
+		$Paranoia = unserialize($Paranoia);
55
+	}
56
+	if (!is_array($Paranoia)) {
57
+		$Paranoia = array();
58
+	}
59
+	if (is_array($Property)) {
60
+		$all = true;
61
+		foreach ($Property as $P) {
62
+			$all = $all && check_paranoia($P, $Paranoia, $UserClass, $UserID);
63
+		}
64
+		return $all;
65
+	} else {
66
+		if (($UserID !== false) && (G::$LoggedUser['ID'] == $UserID)) {
67
+			return PARANOIA_ALLOWED;
68
+		}
69
+
70
+		$May = !in_array($Property, $Paranoia) && !in_array($Property . '+', $Paranoia);
71
+		if ($May)
72
+			return PARANOIA_ALLOWED;
73
+
74
+		if (check_perms('users_override_paranoia', $UserClass)) {
75
+			return PARANOIA_OVERRIDDEN;
76
+		}
77
+		$Override=false;
78
+		switch ($Property) {
79
+			case 'downloaded':
80
+			case 'ratio':
81
+			case 'uploaded':
82
+			case 'lastseen':
83
+				if (check_perms('users_mod', $UserClass))
84
+					return PARANOIA_OVERRIDDEN;
85
+				break;
86
+			case 'snatched': case 'snatched+':
87
+				if (check_perms('users_view_torrents_snatchlist', $UserClass))
88
+					return PARANOIA_OVERRIDDEN;
89
+				break;
90
+			case 'uploads': case 'uploads+':
91
+			case 'seeding': case 'seeding+':
92
+			case 'leeching': case 'leeching+':
93
+				if (check_perms('users_view_seedleech', $UserClass))
94
+					return PARANOIA_OVERRIDDEN;
95
+				break;
96
+			case 'invitedcount':
97
+				if (check_perms('users_view_invites', $UserClass))
98
+					return PARANOIA_OVERRIDDEN;
99
+				break;
100
+		}
101
+		return false;
102
+	}
103
+}

+ 108
- 0
classes/permissions.class.php View File

1
+<?
2
+class Permissions {
3
+	/* Check to see if a user has the permission to perform an action
4
+	 * This is called by check_perms in util.php, for convenience.
5
+	 *
6
+	 * @param string PermissionName
7
+	 * @param string $MinClass Return false if the user's class level is below this.
8
+	 */
9
+  public static function check_perms($PermissionName, $MinClass = 0) {
10
+
11
+    $OverrideClass = 1000;
12
+    $Override = G::$LoggedUser['EffectiveClass'] >= $OverrideClass;
13
+
14
+    return (
15
+      ($PermissionName == null
16
+        || (isset(G::$LoggedUser['Permissions'][$PermissionName])
17
+          && G::$LoggedUser['Permissions'][$PermissionName]))
18
+      && G::$LoggedUser['Permissions'][$PermissionName]
19
+      && (G::$LoggedUser['Class'] >= $MinClass
20
+        || G::$LoggedUser['EffectiveClass'] >= $MinClass
21
+        || $Override)
22
+    );
23
+  }
24
+
25
+	/**
26
+	 * Gets the permissions associated with a certain permissionid
27
+	 *
28
+	 * @param int $PermissionID the kind of permissions to fetch
29
+	 * @return array permissions
30
+	 */
31
+	public static function get_permissions($PermissionID) {
32
+		$Permission = G::$Cache->get_value("perm_$PermissionID");
33
+		if (empty($Permission)) {
34
+			$QueryID = G::$DB->get_query_id();
35
+			G::$DB->query("
36
+				SELECT Level AS Class, `Values` AS Permissions, Secondary, PermittedForums
37
+				FROM permissions
38
+				WHERE ID = '$PermissionID'");
39
+			$Permission = G::$DB->next_record(MYSQLI_ASSOC, array('Permissions'));
40
+			G::$DB->set_query_id($QueryID);
41
+			$Permission['Permissions'] = unserialize($Permission['Permissions']);
42
+			G::$Cache->cache_value("perm_$PermissionID", $Permission, 2592000);
43
+		}
44
+		return $Permission;
45
+	}
46
+
47
+	/**
48
+	 * Get a user's permissions.
49
+	 *
50
+	 * @param $UserID
51
+	 * @param array|false $CustomPermissions
52
+	 *	Pass in the user's custom permissions if you already have them.
53
+	 *	Leave false if you don't have their permissions. The function will fetch them.
54
+	 * @return array Mapping of PermissionName=>bool/int
55
+	 */
56
+	public static function get_permissions_for_user($UserID, $CustomPermissions = false) {
57
+		$UserInfo = Users::user_info($UserID);
58
+
59
+		// Fetch custom permissions if they weren't passed in.
60
+		if ($CustomPermissions === false) {
61
+			$QueryID = G::$DB->get_query_id();
62
+			G::$DB->query('
63
+				SELECT CustomPermissions
64
+				FROM users_main
65
+				WHERE ID = ' . (int)$UserID);
66
+			list($CustomPermissions) = G::$DB->next_record(MYSQLI_NUM, false);
67
+			G::$DB->set_query_id($QueryID);
68
+		}
69
+
70
+		if (!empty($CustomPermissions) && !is_array($CustomPermissions)) {
71
+			$CustomPermissions = unserialize($CustomPermissions);
72
+		}
73
+
74
+		$Permissions = self::get_permissions($UserInfo['PermissionID']);
75
+
76
+		// Manage 'special' inherited permissions
77
+		$BonusPerms = array();
78
+		$BonusCollages = 0;
79
+		foreach ($UserInfo['ExtraClasses'] as $PermID => $Value) {
80
+			$ClassPerms = self::get_permissions($PermID);
81
+			$BonusCollages += $ClassPerms['Permissions']['MaxCollages'];
82
+			unset($ClassPerms['Permissions']['MaxCollages']);
83
+			$BonusPerms = array_merge($BonusPerms, $ClassPerms['Permissions']);
84
+		}
85
+
86
+		if (empty($CustomPermissions)) {
87
+			$CustomPermissions = array();
88
+		}
89
+
90
+		$MaxCollages = $Permissions['Permissions']['MaxCollages'] + $BonusCollages;
91
+		if (isset($CustomPermissions['MaxCollages'])) {
92
+			$MaxCollages += $CustomPermissions['MaxCollages'];
93
+			unset($CustomPermissions['MaxCollages']);
94
+		}
95
+		$Permissions['Permissions']['MaxCollages'] = $MaxCollages;
96
+		// Combine the permissions
97
+		return array_merge(
98
+				$Permissions['Permissions'],
99
+				$BonusPerms,
100
+				$CustomPermissions);
101
+	}
102
+
103
+	public static function is_mod($UserID) {
104
+		$Permissions = self::get_permissions_for_user($UserID);
105
+		return isset($Permissions['users_mod']) && $Permissions['users_mod'];
106
+	}
107
+}
108
+?>

+ 279
- 0
classes/permissions_form.php View File

1
+<?
2
+
3
+/********************************************************************************
4
+ ************ Permissions form ********************** user.php and tools.php ****
5
+ ********************************************************************************
6
+ ** This function is used to create both the class permissions form, and the   **
7
+ ** user custom permissions form.											  **
8
+ ********************************************************************************/
9
+
10
+$PermissionsArray = array(
11
+	'site_leech' => 'Can leech (Does this work?).',
12
+	'site_upload' => 'Upload torrent access.',
13
+	'site_vote' => 'Request vote access.',
14
+	'site_submit_requests' => 'Request create access.',
15
+	'site_advanced_search' => 'Advanced search access.',
16
+	'site_top10' => 'Top 10 access.',
17
+	'site_advanced_top10' => 'Advanced Top 10 access.',
18
+	'site_album_votes' => 'Voting for favorite torrents.',
19
+	'site_torrents_notify' => 'Notifications access.',
20
+	'site_collages_create' => 'Collage create access.',
21
+	'site_collages_manage' => 'Collage manage access.',
22
+	'site_collages_delete' => 'Collage delete access.',
23
+	'site_collages_subscribe' => 'Collage subscription access.',
24
+	'site_collages_personal' => 'Can have a personal collage.',
25
+	'site_collages_renamepersonal' => 'Can rename own personal collages.',
26
+	'site_make_bookmarks' => 'Bookmarks access.',
27
+	'site_edit_wiki' => 'Wiki edit access.',
28
+	'site_can_invite_always' => 'Can invite past user limit.',
29
+	'site_send_unlimited_invites' => 'Unlimited invites.',
30
+	'site_moderate_requests' => 'Request moderation access.',
31
+	'site_delete_artist' => 'Can delete artists (must be able to delete torrents+requests).',
32
+	'site_moderate_forums' => 'Forum moderation access.',
33
+	'site_admin_forums' => 'Forum administrator access.',
34
+	'site_forums_double_post' => 'Can double post in the forums.',
35
+	'site_view_flow' => 'Can view stats and data pools.',
36
+	'site_view_full_log' => 'Can view old log entries.',
37
+	'site_view_torrent_snatchlist' => 'Can view torrent snatch lists.',
38
+	'site_recommend_own' => 'Can recommend own torrents.',
39
+	'site_manage_recommendations' => 'Recommendations management access.',
40
+	'site_delete_tag' => 'Can delete tags.',
41
+	'site_disable_ip_history' => 'Disable IP history.',
42
+	'zip_downloader' => 'Download multiple torrents at once.',
43
+	'site_debug' => 'Developer access.',
44
+	'site_proxy_images' => 'Image proxy & anti-canary.',
45
+	'site_search_many' => 'Can go past low limit of search results.',
46
+	'site_ratio_watch_immunity' => 'Immune from being put on ratio watch.',
47
+	'users_edit_usernames' => 'Can edit usernames.',
48
+	'users_edit_ratio' => 'Can edit anyone\'s upload/download amounts.',
49
+	'users_edit_own_ratio' => 'Can edit own upload/download amounts.',
50
+	'users_edit_titles' => 'Can edit titles.',
51
+	'users_edit_avatars' => 'Can edit avatars.',
52
+	'users_edit_invites' => 'Can edit invite numbers and cancel sent invites.',
53
+	'users_edit_watch_hours' => 'Can edit contrib watch hours.',
54
+	'users_edit_reset_keys' => 'Can reset passkey/authkey.',
55
+	'users_edit_profiles' => 'Can edit anyone\'s profile.',
56
+	'users_view_friends' => 'Can view anyone\'s friends.',
57
+	'users_reset_own_keys' => 'Can reset own passkey/authkey.',
58
+	'users_edit_password' => 'Can change passwords.',
59
+	'users_promote_below' => 'Can promote users to below current level.',
60
+	'users_promote_to' => 'Can promote users up to current level.',
61
+	'users_give_donor' => 'Can give donor access.',
62
+	'users_warn' => 'Can warn users.',
63
+	'users_disable_users' => 'Can disable users.',
64
+	'users_disable_posts' => 'Can disable users\' posting privileges.',
65
+	'users_disable_any' => 'Can disable any users\' rights.',
66
+	'users_delete_users' => 'Can delete users.',
67
+	'users_view_invites' => 'Can view who user has invited.',
68
+	'users_view_seedleech' => 'Can view what a user is seeding or leeching.',
69
+	'users_view_uploaded' => 'Can view a user\'s uploads, regardless of privacy level.',
70
+	'users_view_keys' => 'Can view passkeys.',
71
+	'users_view_ips' => 'Can view IP addresses.',
72
+	'users_view_email' => 'Can view email addresses.',
73
+	'users_invite_notes' => 'Can add a staff note when inviting someone.',
74
+	'users_override_paranoia' => 'Can override paranoia.',
75
+	'users_logout' => 'Can log users out (old?).',
76
+	'users_make_invisible' => 'Can make users invisible.',
77
+	'users_mod' => 'Basic moderator tools.',
78
+	'torrents_edit' => 'Can edit any torrent.',
79
+	'torrents_delete' => 'Can delete torrents.',
80
+	'torrents_delete_fast' => 'Can delete more than 3 torrents at a time.',
81
+	'torrents_freeleech' => 'Can make torrents freeleech.',
82
+	'torrents_search_fast' => 'Rapid search (for scripts).',
83
+	'torrents_hide_dnu' => 'Hide the Do Not Upload list by default.',
84
+	'torrents_fix_ghosts' => 'Can fix "ghost" groups on artist pages.',
85
+	'screenshots_add' => 'Can add screenshots to any torrent and delete their own screenshots.',
86
+	'screenshots_delete' => 'Can delete any screenshot from any torrent.',
87
+	'admin_manage_news' => 'Can manage site news.',
88
+	'admin_manage_blog' => 'Can manage the site blog.',
89
+	'admin_manage_polls' => 'Can manage polls.',
90
+	'admin_manage_forums' => 'Can manage forums (add/edit/delete).',
91
+	'admin_manage_fls' => 'Can manage FLS.',
92
+	'admin_reports' => 'Can access reports system.',
93
+	'admin_advanced_user_search' => 'Can access advanced user search.',
94
+	'admin_create_users' => 'Can create users through an administrative form.',
95
+	'admin_donor_log' => 'Can view the donor log.',
96
+	'admin_manage_ipbans' => 'Can manage IP bans.',
97
+	'admin_dnu' => 'Can manage do not upload list.',
98
+	'admin_clear_cache' => 'Can clear cached.',
99
+	'admin_whitelist' => 'Can manage the list of allowed clients.',
100
+	'admin_manage_permissions' => 'Can edit permission classes/user permissions.',
101
+	'admin_schedule' => 'Can run the site schedule.',
102
+	'admin_login_watch' => 'Can manage login watch.',
103
+	'admin_manage_wiki' => 'Can manage wiki access.',
104
+	'admin_update_geoip' => 'Can update geoIP data.',
105
+	'site_collages_recover' => 'Can recover \'deleted\' collages.',
106
+	'torrents_add_artist' => 'Can add artists to any group.',
107
+	'edit_unknowns' => 'Can edit unknown release information.',
108
+	'forums_polls_create' => 'Can create polls in the forums.',
109
+	'forums_polls_moderate' => 'Can feature and close polls.',
110
+	'project_team' => 'Is part of the project team.',
111
+	'torrents_edit_vanityhouse' => 'Can mark groups as part of Vanity House.',
112
+	'artist_edit_vanityhouse' => 'Can mark artists as part of Vanity House.',
113
+	'site_tag_aliases_read' => 'Can view the list of tag aliases.'
114
+);
115
+
116
+function permissions_form() {
117
+?>
118
+<div class="permissions">
119
+	<div class="permission_container">
120
+		<table>
121
+			<tr class="colhead">
122
+				<td>Site</td>
123
+			</tr>
124
+			<tr>
125
+				<td>
126
+<?
127
+					display_perm('site_leech','Can leech.');
128
+					display_perm('site_upload','Can upload.');
129
+					display_perm('site_vote','Can vote on requests.');
130
+					display_perm('site_submit_requests','Can submit requests.');
131
+					display_perm('site_advanced_search','Can use advanced search.');
132
+					display_perm('site_top10','Can access top 10.');
133
+					display_perm('site_torrents_notify','Can access torrents notifications system.');
134
+					display_perm('site_collages_create','Can create collages.');
135
+					display_perm('site_collages_manage','Can manage collages (add torrents, sorting).');
136
+					display_perm('site_collages_delete','Can delete collages.');
137
+					display_perm('site_collages_subscribe','Can access collage subscriptions.');
138
+					display_perm('site_collages_personal','Can have a personal collage.');
139
+					display_perm('site_collages_renamepersonal','Can rename own personal collages.');
140
+					display_perm('site_advanced_top10','Can access advanced top 10.');
141
+					display_perm('site_album_votes', 'Can vote for favorite torrents.');
142
+					display_perm('site_make_bookmarks','Can make bookmarks.');
143
+					display_perm('site_edit_wiki','Can edit wiki pages.');
144
+					display_perm('site_can_invite_always', 'Can invite users even when invites are closed.');
145
+					display_perm('site_send_unlimited_invites', 'Can send unlimited invites.');
146
+					display_perm('site_moderate_requests', 'Can moderate any request.');
147
+					display_perm('site_delete_artist', 'Can delete artists (must be able to delete torrents+requests).');
148
+					display_perm('forums_polls_create','Can create polls in the forums.');
149
+					display_perm('forums_polls_moderate','Can feature and close polls.');
150
+					display_perm('site_moderate_forums', 'Can moderate the forums.');
151
+					display_perm('site_admin_forums', 'Can administrate the forums.');
152
+					display_perm('site_view_flow', 'Can view site stats and data pools.');
153
+					display_perm('site_view_full_log', 'Can view the full site log.');
154
+					display_perm('site_view_torrent_snatchlist', 'Can view torrent snatch lists.');
155
+					display_perm('site_recommend_own', 'Can add own torrents to recommendations list.');
156
+					display_perm('site_manage_recommendations', 'Can edit recommendations list.');
157
+					display_perm('site_delete_tag', 'Can delete tags.');
158
+					display_perm('site_disable_ip_history', 'Disable IP history.');
159
+					display_perm('zip_downloader', 'Download multiple torrents at once.');
160
+					display_perm('site_debug', 'View site debug tables.');
161
+					display_perm('site_proxy_images', 'Proxy images through the server.');
162
+					display_perm('site_search_many', 'Can go past low limit of search results.');
163
+					display_perm('site_collages_recover', 'Can recover \'deleted\' collages.');
164
+					display_perm('site_forums_double_post', 'Can double post in the forums.');
165
+					display_perm('project_team', 'Part of the project team.');
166
+					display_perm('site_tag_aliases_read', 'Can view the list of tag aliases.');
167
+					display_perm('site_ratio_watch_immunity', 'Immune from being put on ratio watch.');
168
+?>
169
+				</td>
170
+			</tr>
171
+		</table>
172
+	</div>
173
+	<div class="permission_container">
174
+		<table>
175
+			<tr class="colhead">
176
+				<td>Users</td>
177
+			</tr>
178
+			<tr>
179
+				<td>
180
+<?
181
+					display_perm('users_edit_usernames', 'Can edit usernames.');
182
+					display_perm('users_edit_ratio', 'Can edit anyone\'s upload/download amounts.');
183
+					display_perm('users_edit_own_ratio', 'Can edit own upload/download amounts.');
184
+					display_perm('users_edit_titles', 'Can edit titles.');
185
+					display_perm('users_edit_avatars', 'Can edit avatars.');
186
+					display_perm('users_edit_invites', 'Can edit invite numbers and cancel sent invites.');
187
+					display_perm('users_edit_watch_hours', 'Can edit contrib watch hours.');
188
+					display_perm('users_edit_reset_keys', 'Can reset any passkey/authkey.');
189
+					display_perm('users_edit_profiles', 'Can edit anyone\'s profile.');
190
+					display_perm('users_edit_badges', 'Can edit anyone\'s badges.');
191
+					display_perm('users_view_friends', 'Can view anyone\'s friends.');
192
+					display_perm('users_reset_own_keys', 'Can reset own passkey/authkey.');
193
+					display_perm('users_edit_password', 'Can change password.');
194
+					display_perm('users_promote_below', 'Can promote users to below current level.');
195
+					display_perm('users_promote_to', 'Can promote users up to current level.');
196
+					display_perm('users_give_donor', 'Can give donor access.');
197
+					display_perm('users_warn', 'Can warn users.');
198
+					display_perm('users_disable_users', 'Can disable users.');
199
+					display_perm('users_disable_posts', 'Can disable users\' posting privileges.');
200
+					display_perm('users_disable_any', 'Can disable any users\' rights.');
201
+					display_perm('users_delete_users', 'Can delete anyone\'s account');
202
+					display_perm('users_view_invites', 'Can view who user has invited');
203
+					display_perm('users_view_seedleech', 'Can view what a user is seeding or leeching');
204
+					display_perm('users_view_uploaded', 'Can view a user\'s uploads, regardless of privacy level');
205
+					display_perm('users_view_keys', 'Can view passkeys');
206
+					display_perm('users_view_ips', 'Can view IP addresses');
207
+					display_perm('users_view_email', 'Can view email addresses');
208
+					display_perm('users_invite_notes', 'Can add a staff note when inviting someone.');
209
+					display_perm('users_override_paranoia', 'Can override paranoia');
210
+					display_perm('users_make_invisible', 'Can make users invisible');
211
+					display_perm('users_logout', 'Can log users out');
212
+					display_perm('users_mod', 'Can access basic moderator tools (Admin comment)');
213
+?>
214
+					*Everything is only applicable to users with the same or lower class level
215
+				</td>
216
+			</tr>
217
+		</table>
218
+	</div>
219
+	<div class="permission_container">
220
+		<table>
221
+			<tr class="colhead">
222
+				<td>Torrents</td>
223
+			</tr>
224
+			<tr>
225
+				<td>
226
+<?
227
+					display_perm('torrents_edit', 'Can edit any torrent');
228
+					display_perm('torrents_delete', 'Can delete torrents');
229
+					display_perm('torrents_delete_fast', 'Can delete more than 3 torrents at a time.');
230
+					display_perm('torrents_freeleech', 'Can make torrents freeleech');
231
+					display_perm('torrents_search_fast', 'Unlimit search frequency (for scripts).');
232
+					display_perm('torrents_add_artist', 'Can add artists to any group.');
233
+					display_perm('edit_unknowns', 'Can edit unknown release information.');
234
+					display_perm('torrents_edit_vanityhouse', 'Can mark groups as part of Vanity House.');
235
+					display_perm('artist_edit_vanityhouse', 'Can mark artists as part of Vanity House.');
236
+					display_perm('torrents_hide_dnu', 'Hide the Do Not Upload list by default.');
237
+					display_perm('torrents_fix_ghosts', 'Can fix ghost groups on artist pages.');
238
+					display_perm('screenshots_add', 'Can add screenshots to any torrent and delete their own screenshots.');
239
+					display_perm('screenshots_delete', 'Can delete any screenshot from any torrent.');
240
+?>
241
+				</td>
242
+			</tr>
243
+		</table>
244
+	</div>
245
+	<div class="permission_container">
246
+		<table>
247
+			<tr class="colhead">
248
+				<td>Administrative</td>
249
+			</tr>
250
+			<tr>
251
+				<td>
252
+<?
253
+					display_perm('admin_manage_news', 'Can manage site news');
254
+					display_perm('admin_manage_blog', 'Can manage the site blog');
255
+					display_perm('admin_manage_polls', 'Can manage polls');
256
+					display_perm('admin_manage_forums', 'Can manage forums (add/edit/delete)');
257
+					display_perm('admin_manage_fls', 'Can manage FLS');
258
+					display_perm('admin_reports', 'Can access reports system');
259
+					display_perm('admin_advanced_user_search', 'Can access advanced user search');
260
+					display_perm('admin_create_users', 'Can create users through an administrative form');
261
+					display_perm('admin_donor_log', 'Can view the donor log');
262
+					display_perm('admin_manage_ipbans', 'Can manage IP bans');
263
+					display_perm('admin_dnu', 'Can manage do not upload list');
264
+					display_perm('admin_clear_cache', 'Can clear cached pages');
265
+					display_perm('admin_whitelist', 'Can manage the list of allowed clients.');
266
+					display_perm('admin_manage_permissions', 'Can edit permission classes/user permissions.');
267
+					display_perm('admin_schedule', 'Can run the site schedule.');
268
+					display_perm('admin_login_watch', 'Can manage login watch.');
269
+					display_perm('admin_manage_wiki', 'Can manage wiki access.');
270
+					display_perm('admin_update_geoip', 'Can update geoIP data.');
271
+?>
272
+				</td>
273
+			</tr>
274
+		</table>
275
+	</div>
276
+	<div class="submit_container"><input type="submit" name="submit" value="Save Permission Class" /></div>
277
+</div>
278
+<?
279
+}

+ 43
- 0
classes/proxies.class.php View File

1
+<?
2
+//Useful: http://www.robtex.com/cnet/
3
+$AllowedProxies = array(
4
+	//Opera Turbo (may include Opera-owned IP addresses that aren't used for Turbo, but shouldn't run much risk of exploitation)
5
+	'64.255.180.*', //Norway
6
+	'64.255.164.*', //Norway
7
+	'80.239.242.*', //Poland
8
+	'80.239.243.*', //Poland
9
+	'91.203.96.*', //Norway
10
+	'94.246.126.*', //Norway
11
+	'94.246.127.*', //Norway
12
+	'195.189.142.*', //Norway
13
+	'195.189.143.*', //Norway
14
+);
15
+
16
+function proxyCheck($IP) {
17
+	global $AllowedProxies;
18
+	for ($i = 0, $il = count($AllowedProxies); $i < $il; ++$i) {
19
+		//based on the wildcard principle it should never be shorter
20
+		if (strlen($IP) < strlen($AllowedProxies[$i])) {
21
+			continue;
22
+		}
23
+
24
+		//since we're matching bit for bit iterating from the start
25
+		for ($j = 0, $jl = strlen($IP); $j < $jl; ++$j) {
26
+			//completed iteration and no inequality
27
+			if ($j == $jl - 1 && $IP[$j] === $AllowedProxies[$i][$j]) {
28
+				return true;
29
+			}
30
+
31
+			//wildcard
32
+			if ($AllowedProxies[$i][$j] === '*') {
33
+				return true;
34
+			}
35
+
36
+			//inequality found
37
+			if ($IP[$j] !== $AllowedProxies[$i][$j]) {
38
+				break;
39
+			}
40
+		}
41
+	}
42
+	return false;
43
+}

+ 184
- 0
classes/pushserver.class.php View File

1
+<?php
2
+define("PUSH_SOCKET_LISTEN_ADDRESS", "127.0.0.1");
3
+define("PUSH_SOCKET_LISTEN_PORT", 6789);
4
+
5
+require 'NMA_API.php';
6
+require 'config.php';
7
+class PushServer {
8
+	private $ListenSocket = false;
9
+	private $State = 1;
10
+	private $Listened = false;
11
+
12
+	public function __construct() {
13
+		// restore_error_handler(); //Avoid PHP error logging
14
+		set_time_limit(0);
15
+		$this->init();
16
+		$this->listen();
17
+	}
18
+
19
+	private function init() {
20
+		$this->ListenSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
21
+		socket_set_option($this->ListenSocket, SOL_SOCKET, SO_REUSEADDR, 1);
22
+		socket_bind($this->ListenSocket, PUSH_SOCKET_LISTEN_ADDRESS, PUSH_SOCKET_LISTEN_PORT);
23
+		socket_listen($this->ListenSocket);
24
+		socket_set_nonblock($this->ListenSocket);
25
+		echo "\nInitialized\n";
26
+	}
27
+
28
+	private function listen() {
29
+		echo "\nListening...\n";
30
+		while ( ($this->State) == 1 ) {
31
+			if ($this->Listened = @socket_accept($this->ListenSocket)) {
32
+				$Data = socket_read($this->Listened, 512);
33
+				$this->parse_data($Data);
34
+			}
35
+			usleep(5000);
36
+		}
37
+	}
38
+
39
+	private function parse_data($Data) {
40
+		$JSON = json_decode($Data, true);
41
+		$Service = strtolower($JSON['service']);
42
+		switch ($Service) {
43
+			case 'nma':
44
+				$this->push_nma($JSON['user']['key'], $JSON['message']['title'], $JSON['message']['body'], $JSON['message']['url']);
45
+				break;
46
+			case 'prowl':
47
+				$this->push_prowl($JSON['user']['key'], $JSON['message']['title'], $JSON['message']['body'], $JSON['message']['url']);
48
+				break;
49
+			case 'toasty':
50
+				$this->push_toasty($JSON['user']['key'], $JSON['message']['title'], $JSON['message']['body'], $JSON['message']['url']);
51
+				break;
52
+			case 'pushover':
53
+				$this->push_pushover($JSON['user']['key'], $JSON['message']['title'], $JSON['message']['body'], $JSON['message']['url']);
54
+				break;
55
+			case 'pushbullet':
56
+				$this->push_pushbullet(
57
+					$JSON['user']['key'],
58
+					$JSON['user']['device'],
59
+					$JSON['message']['title'],
60
+					$JSON['message']['body'],
61
+					$JSON['message']['url']
62
+				);
63
+			default:
64
+				break;
65
+		}
66
+	}
67
+
68
+	private function push_prowl($Key, $Title, $Message, $URL) {
69
+		$API = "https://api.prowlapp.com/publicapi/add";
70
+		$Fields = array(
71
+				'apikey' => urlencode($Key),
72
+				'application' => urlencode(SITE_NAME),
73
+				'event' => urlencode($Title),
74
+				'description' => urlencode($Message)
75
+		);
76
+		if (!empty($URL)) {
77
+			$Fields['url'] = $URL;
78
+		}
79
+		$FieldsString = "";
80
+		foreach ($Fields as $key => $value) {
81
+			$FieldsString .= $key . '=' . $value . '&';
82
+		}
83
+		rtrim($FieldsString, '&');
84
+
85
+		$Curl = curl_init();
86
+		curl_setopt($Curl, CURLOPT_URL, $API);
87
+		curl_setopt($Curl, CURLOPT_POST, count($Fields));
88
+		curl_setopt($Curl, CURLOPT_POSTFIELDS, $FieldsString);
89
+		curl_exec($Curl);
90
+		curl_close($Curl);
91
+		echo "Push sent to Prowl";
92
+	}
93
+
94
+	private function push_toasty($Key, $Title, $Message, $URL) {
95
+		$API = "http://api.supertoasty.com/notify/" . urlencode($Key) . "?";
96
+		if (!empty($URL)) {
97
+			$Message = $Message . " " . $URL;
98
+		}
99
+		$Fields = array(
100
+				'title' => urlencode($Title),
101
+				'text' => urlencode($Message),
102
+				'sender' => urlencode(SITE_NAME)
103
+		);
104
+		$FieldsString = "";
105
+		foreach ($Fields as $key => $value) {
106
+			$FieldsString .= $key . '=' . $value . '&';
107
+		}
108
+		rtrim($FieldsString, '&');
109
+
110
+		$Curl = curl_init();
111
+		curl_setopt($Curl, CURLOPT_URL, $API);
112
+		curl_setopt($Curl, CURLOPT_POST, count($Fields));
113
+		curl_setopt($Curl, CURLOPT_POSTFIELDS, $FieldsString);
114
+		curl_exec($Curl);
115
+		curl_close($Curl);
116
+		echo "Push sent to Toasty";
117
+	}
118
+
119
+	private function push_nma($Key, $Title, $Message, $URL) {
120
+		$NMA = new NMA_API(array(
121
+				'apikey' => $Key
122
+		));
123
+		if ($NMA->verify()) {
124
+			if ($NMA->notify(SITE_NAME, $Title, $Message, $URL)) {
125
+				echo "Push sent to NMA";
126
+			}
127
+		}
128
+	}
129
+
130
+	private function push_pushover($UserKey, $Title, $Message, $URL) {
131
+		curl_setopt_array($ch = curl_init(), array(
132
+				CURLOPT_URL => "https://api.pushover.net/1/messages.json",
133
+				CURLOPT_POSTFIELDS => array(
134
+						"token" => PUSHOVER_KEY,
135
+						"user" => $UserKey,
136
+						"title" => $Title,
137
+						"message" => $Message,
138
+						"url" => $URL
139
+				)
140
+		));
141
+		curl_exec($ch);
142
+		curl_close($ch);
143
+		echo "Push sent to Pushover";
144
+	}
145
+
146
+	/**
147
+	 * Notify via pushbullet
148
+	 *
149
+	 * @param $UserKey User API key
150
+	 * @param $DeviceID device to push to
151
+	 * @param $Title Notification title
152
+	 * @param $Message Notification message
153
+	 * @param $URL For compatibility with other command. Just gets appended.
154
+	 */
155
+	private function push_pushbullet($UserKey, $DeviceID,
156
+		$Title, $Message, $URL) {
157
+		if (!empty($URL)) {
158
+			$Message .= ' ' . $URL;
159
+		}
160
+
161
+		curl_setopt_array($Curl = curl_init(), array(
162
+			CURLOPT_URL => 'https://api.pushbullet.com/api/pushes',
163
+			CURLOPT_POSTFIELDS => array(
164
+				'type' => 'note',
165
+				'title' => $Title,
166
+				'body' => $Message,
167
+				'device_iden' => $DeviceID
168
+			),
169
+			CURLOPT_USERPWD => $UserKey . ':',
170
+			CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
171
+			CURLOPT_RETURNTRANSFER => True
172
+		));
173
+
174
+		$Result = curl_exec($Curl);
175
+		echo "Push sent to Pushbullet";
176
+		curl_close($Curl);
177
+
178
+
179
+
180
+	}
181
+}
182
+
183
+$PushServer = new PushServer();
184
+?>

+ 16
- 0
classes/regex.php View File

1
+<?
2
+//resource_type://username:password@domain:port/path?query_string#anchor
3
+define('RESOURCE_REGEX', '(https?|ftps?):\/\/');
4
+define('IP_REGEX', '(\d{1,3}\.){3}\d{1,3}');
5
+define('DOMAIN_REGEX', '([a-z0-9\-\_]+\.)*[a-z0-9\-\_]+');
6
+define('PORT_REGEX', ':\d{1,5}');
7
+define('URL_REGEX', '('.RESOURCE_REGEX.')('.IP_REGEX.'|'.DOMAIN_REGEX.')('.PORT_REGEX.')?(\/\S*)*');
8
+define('USERNAME_REGEX', '/^[a-z0-9_]{2,20}$/iD');
9
+define('EMAIL_REGEX','[_a-z0-9-]+([.+][_a-z0-9-]+)*@'.DOMAIN_REGEX);
10
+define('IMAGE_REGEX', URL_REGEX.'\/\S+\.(jpg|jpeg|tif|tiff|png|gif|bmp)(\?\S*)?');
11
+define('VIDEO_REGEX', URL_REGEX.'\/\S+\.(webm)(\?\S*)?');
12
+define('CSS_REGEX', URL_REGEX.'\/\S+\.css(\?\S*)?');
13
+define('SITELINK_REGEX', RESOURCE_REGEX.'(ssl.)?'.preg_quote(NONSSL_SITE_URL, '/'));
14
+define('TORRENT_REGEX', SITELINK_REGEX.'\/torrents\.php\?(.*&)?torrentid=(\d+)'); // torrentid = group 4
15
+define('TORRENT_GROUP_REGEX', SITELINK_REGEX.'\/torrents\.php\?(.*&)?id=(\d+)'); // id = group 4
16
+define('ARTIST_REGEX', SITELINK_REGEX.'\/artist\.php\?(.*&)?id=(\d+)'); // id = group 4

+ 27
- 0
classes/reports.class.php View File

1
+<?php
2
+class Reports {
3
+	/**
4
+	 * This function formats a string containing a torrent's remaster information
5
+	 * to be used in Reports v2.
6
+	 *
7
+	 * @param boolean  $Remastered - whether the torrent contains remaster information
8
+	 * @param string   $RemasterTitle - the title of the remaster information
9
+	 * @param string   $RemasterYear - the year of the remaster information
10
+	 */
11
+	public static function format_reports_remaster_info($Remastered, $RemasterTitle, $RemasterYear) {
12
+		if ($Remastered) {
13
+			$RemasterDisplayString = ' &lt;';
14
+			if ($RemasterTitle != '' && $RemasterYear != '') {
15
+				$RemasterDisplayString .= "$RemasterTitle - $RemasterYear";
16
+			} elseif ($RemasterTitle != '' && $RemasterYear == '') {
17
+				$RemasterDisplayString .= $RemasterTitle;
18
+			} elseif ($RemasterTitle == '' && $RemasterYear != '') {
19
+				$RemasterDisplayString .= $RemasterYear;
20
+			}
21
+			$RemasterDisplayString .= '&gt;';
22
+		} else {
23
+			$RemasterDisplayString = '';
24
+		}
25
+		return $RemasterDisplayString;
26
+	}
27
+}

+ 235
- 0
classes/requests.class.php View File

1
+<?
2
+class Requests {
3
+	/**
4
+	 * Update the sphinx requests delta table for a request.
5
+	 *
6
+	 * @param $RequestID
7
+	 */
8
+	public static function update_sphinx_requests($RequestID) {
9
+		$QueryID = G::$DB->get_query_id();
10
+
11
+		G::$DB->query("
12
+			SELECT REPLACE(t.Name, '.', '_')
13
+			FROM tags AS t
14
+				JOIN requests_tags AS rt ON t.ID = rt.TagID
15
+			WHERE rt.RequestID = $RequestID");
16
+		$TagList = G::$DB->collect(0, false);
17
+		$TagList = db_string(implode(' ', $TagList));
18
+
19
+		G::$DB->query("
20
+			REPLACE INTO sphinx_requests_delta (
21
+				ID, UserID, TimeAdded, LastVote, CategoryID, Title, TagList,
22
+				CatalogueNumber, DLSiteID, FillerID, TorrentID,
23
+				TimeFilled, Visible, Votes, Bounty)
24
+			SELECT
25
+				ID, r.UserID, UNIX_TIMESTAMP(TimeAdded) AS TimeAdded,
26
+				UNIX_TIMESTAMP(LastVote) AS LastVote, CategoryID, Title, '$TagList',
27
+				CatalogueNumber, DLSiteID, FillerID, TorrentID,
28
+				UNIX_TIMESTAMP(TimeFilled) AS TimeFilled, Visible,
29
+				COUNT(rv.UserID) AS Votes, SUM(rv.Bounty) >> 10 AS Bounty
30
+			FROM requests AS r
31
+				LEFT JOIN requests_votes AS rv ON rv.RequestID = r.ID
32
+			WHERE ID = $RequestID
33
+			GROUP BY r.ID");
34
+		G::$DB->query("
35
+			UPDATE sphinx_requests_delta
36
+			SET ArtistList = (
37
+					SELECT GROUP_CONCAT(ag.Name SEPARATOR ' ')
38
+					FROM requests_artists AS ra
39
+						JOIN artists_group AS ag ON ag.ArtistID = ra.ArtistID
40
+					WHERE ra.RequestID = $RequestID
41
+					GROUP BY NULL
42
+					)
43
+			WHERE ID = $RequestID");
44
+		G::$DB->set_query_id($QueryID);
45
+
46
+		G::$Cache->delete_value("request_$RequestID");
47
+	}
48
+
49
+
50
+
51
+	/**
52
+	 * Function to get data from an array of $RequestIDs. Order of keys doesn't matter (let's keep it that way).
53
+	 *
54
+	 * @param array $RequestIDs
55
+	 * @param boolean $Return if set to false, data won't be returned (ie. if we just want to prime the cache.)
56
+	 * @return The array of requests.
57
+	 * Format: array(RequestID => Associative array)
58
+	 * To see what's exactly inside each associate array, peek inside the function. It won't bite.
59
+	 */
60
+	//
61
+	//In places where the output from this is merged with sphinx filters, it will be in a different order.
62
+	public static function get_requests($RequestIDs, $Return = true) {
63
+		$Found = $NotFound = array_fill_keys($RequestIDs, false);
64
+		// Try to fetch the requests from the cache first.
65
+		foreach ($RequestIDs as $i => $RequestID) {
66
+			if (!is_number($RequestID)) {
67
+				unset($RequestIDs[$i], $Found[$GroupID], $NotFound[$GroupID]);
68
+				continue;
69
+			}
70
+			$Data = G::$Cache->get_value("request_$RequestID");
71
+			if (!empty($Data)) {
72
+				unset($NotFound[$RequestID]);
73
+				$Found[$RequestID] = $Data;
74
+			}
75
+		}
76
+		// Make sure there's something in $RequestIDs, otherwise the SQL will break
77
+		if (count($RequestIDs) === 0) {
78
+			return array();
79
+		}
80
+		$IDs = implode(',', array_keys($NotFound));
81
+
82
+		/*
83
+			Don't change without ensuring you change everything else that uses get_requests()
84
+		*/
85
+
86
+		if (count($NotFound) > 0) {
87
+			$QueryID = G::$DB->get_query_id();
88
+
89
+			G::$DB->query("
90
+				SELECT
91
+					ID,
92
+					UserID,
93
+					TimeAdded,
94
+					LastVote,
95
+					CategoryID,
96
+					Title,
97
+					TitleJP,
98
+					Image,
99
+					Description,
100
+					CatalogueNumber,
101
+					DLsiteID,
102
+					FillerID,
103
+					TorrentID,
104
+					TimeFilled,
105
+					GroupID
106
+				FROM requests
107
+				WHERE ID IN ($IDs)
108
+				ORDER BY ID");
109
+			$Requests = G::$DB->to_array(false, MYSQLI_ASSOC, true);
110
+			$Tags = self::get_tags(G::$DB->collect('ID', false));
111
+			foreach ($Requests as $Request) {
112
+				unset($NotFound[$Request['ID']]);
113
+				$Request['Tags'] = isset($Tags[$Request['ID']]) ? $Tags[$Request['ID']] : array();
114
+				$Found[$Request['ID']] = $Request;
115
+				G::$Cache->cache_value('request_'.$Request['ID'], $Request, 0);
116
+			}
117
+			G::$DB->set_query_id($QueryID);
118
+
119
+			// Orphan requests. There shouldn't ever be any
120
+			if (count($NotFound) > 0) {
121
+				foreach (array_keys($NotFound) as $GroupID) {
122
+					unset($Found[$GroupID]);
123
+				}
124
+			}
125
+		}
126
+
127
+		if ($Return) { // If we're interested in the data, and not just caching it
128
+			return $Found;
129
+		}
130
+	}
131
+
132
+	/**
133
+	 * Return a single request. Wrapper for get_requests
134
+	 *
135
+	 * @param int $RequestID
136
+	 * @return request array or false if request doesn't exist. See get_requests for a description of the format
137
+	 */
138
+	public static function get_request($RequestID) {
139
+		$Request = self::get_requests(array($RequestID));
140
+		if (isset($Request[$RequestID])) {
141
+			return $Request[$RequestID];
142
+		}
143
+		return false;
144
+	}
145
+
146
+	public static function get_artists($RequestID) {
147
+		$Artists = G::$Cache->get_value("request_artists_$RequestID");
148
+		if (is_array($Artists)) {
149
+			$Results = $Artists;
150
+		} else {
151
+			$Results = array();
152
+			$QueryID = G::$DB->get_query_id();
153
+			G::$DB->query("
154
+				SELECT
155
+					ra.ArtistID,
156
+					ag.Name
157
+				FROM requests_artists AS ra
158
+					JOIN artists_group AS ag ON ra.ArtistID = ag.ArtistID
159
+				WHERE ra.RequestID = $RequestID
160
+				ORDER BY ag.Name ASC;");
161
+			$ArtistRaw = G::$DB->to_array();
162
+			G::$DB->set_query_id($QueryID);
163
+			foreach ($ArtistRaw as $ArtistRow) {
164
+				list($ArtistID, $ArtistName) = $ArtistRow;
165
+				$Results[] = array('id' => $ArtistID, 'name' => $ArtistName);
166
+			}
167
+			G::$Cache->cache_value("request_artists_$RequestID", $Results);
168
+		}
169
+		return $Results;
170
+	}
171
+
172
+	public static function get_tags($RequestIDs) {
173
+		if (empty($RequestIDs)) {
174
+			return array();
175
+		}
176
+		if (is_array($RequestIDs)) {
177
+			$RequestIDs = implode(',', $RequestIDs);
178
+		}
179
+		$QueryID = G::$DB->get_query_id();
180
+		G::$DB->query("
181
+			SELECT
182
+				rt.RequestID,
183
+				rt.TagID,
184
+				t.Name
185
+			FROM requests_tags AS rt
186
+				JOIN tags AS t ON rt.TagID = t.ID
187
+			WHERE rt.RequestID IN ($RequestIDs)
188
+			ORDER BY rt.TagID ASC");
189
+		$Tags = G::$DB->to_array(false, MYSQLI_NUM, false);
190
+		G::$DB->set_query_id($QueryID);
191
+		$Results = array();
192
+		foreach ($Tags as $TagsRow) {
193
+			list($RequestID, $TagID, $TagName) = $TagsRow;
194
+			$Results[$RequestID][$TagID] = $TagName;
195
+		}
196
+		return $Results;
197
+	}
198
+
199
+	public static function get_votes_array($RequestID) {
200
+		$RequestVotes = G::$Cache->get_value("request_votes_$RequestID");
201
+		if (!is_array($RequestVotes)) {
202
+			$QueryID = G::$DB->get_query_id();
203
+			G::$DB->query("
204
+				SELECT
205
+					rv.UserID,
206
+					rv.Bounty,
207
+					u.Username
208
+				FROM requests_votes AS rv
209
+					LEFT JOIN users_main AS u ON u.ID = rv.UserID
210
+				WHERE rv.RequestID = $RequestID
211
+				ORDER BY rv.Bounty DESC");
212
+			if (!G::$DB->has_results()) {
213
+				return array(
214
+					'TotalBounty' => 0,
215
+					'Voters' => array());
216
+			}
217
+			$Votes = G::$DB->to_array();
218
+
219
+			$RequestVotes = array();
220
+			$RequestVotes['TotalBounty'] = array_sum(G::$DB->collect('Bounty'));
221
+
222
+			foreach ($Votes as $Vote) {
223
+				list($UserID, $Bounty, $Username) = $Vote;
224
+				$VoteArray = array();
225
+				$VotesArray[] = array('UserID' => $UserID, 'Username' => $Username, 'Bounty' => $Bounty);
226
+			}
227
+
228
+			$RequestVotes['Voters'] = $VotesArray;
229
+			G::$Cache->cache_value("request_votes_$RequestID", $RequestVotes);
230
+			G::$DB->set_query_id($QueryID);
231
+		}
232
+		return $RequestVotes;
233
+	}
234
+
235
+}

+ 29
- 0
classes/revisionhistory.class.php View File

1
+<?
2
+class RevisionHistory {
3
+	/**
4
+	 * Read the revision history of an artist or torrent page
5
+	 * @param string $Page artists or torrents
6
+	 * @param in $PageID
7
+	 * @return array
8
+	 */
9
+	public static function get_revision_history($Page, $PageID) {
10
+		if ($Page == 'artists') {
11
+			$Table = 'wiki_artists';
12
+		} else {
13
+			$Table = 'wiki_torrents';
14
+		}
15
+		$QueryID = G::$DB->get_query_id();
16
+		G::$DB->query("
17
+			SELECT
18
+				RevisionID,
19
+				Summary,
20
+				Time,
21
+				UserID
22
+			FROM $Table
23
+			WHERE PageID = $PageID
24
+			ORDER BY RevisionID DESC");
25
+		$Ret = G::$DB->to_array();
26
+		G::$DB->set_query_id($QueryID);
27
+		return $Ret;
28
+	}
29
+}

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

1
+<?
2
+class RevisionHistoryView {
3
+	/**
4
+	 * Render the revision history
5
+	 * @param array $RevisionHistory see RevisionHistory::get_revision_history
6
+	 * @param string $BaseURL
7
+	 */
8
+	public static function render_revision_history($RevisionHistory, $BaseURL) {
9
+?>
10
+	<table cellpadding="6" cellspacing="1" border="0" width="100%" class="box">
11
+		<tr class="colhead">
12
+			<td>Revision</td>
13
+			<td>Date</td>
14
+			<td>User</td>
15
+			<td>Summary</td>
16
+		</tr>
17
+<?
18
+		foreach ($RevisionHistory as $Entry) {
19
+			list($RevisionID, $Summary, $Time, $UserID) = $Entry;
20
+?>
21
+		<tr class="row">
22
+			<td>
23
+				<?= "<a href=\"$BaseURL&amp;revisionid=$RevisionID\">#$RevisionID</a>" ?>
24
+			</td>
25
+			<td>
26
+				<?=$Time?>
27
+			</td>
28
+			<td>
29
+				<?=Users::format_username($UserID, false, false, false)?>
30
+			</td>
31
+			<td>
32
+				<?=($Summary ? $Summary : '(empty)')?>
33
+			</td>
34
+		</tr>
35
+<?		} ?>
36
+	</table>
37
+<?
38
+	}
39
+}

+ 171
- 0
classes/rules.class.php View File

1
+<?php
2
+class Rules {
3
+
4
+	/**
5
+	 * Displays the site's "Golden Rules".
6
+	 *
7
+	 */
8
+	public static function display_golden_rules() {
9
+		?>
10
+		<ol>
11
+			<li>All staff decisions must be respected. If you take issue with a decision, you must do so privately with the staff member who issued the decision or with an administrator of the site. Complaining about staff decisions in public or otherwise disrespecting staff members WILL result in the offender being called a faggot.</li>
12
+			<li>Access to this website is a gift and a privilege, and it can be taken away from you for any reason (or no reason).</li>
13
+			<li>One account per person per lifetime. Anyone creating additional accounts will probably be banned. Additionally, unless your account is immune to <a href="wiki.php?action=article&name=inactivitypruning">inactivity pruning</a>, accounts are automatically disabled if one page load is not made at least once every four months.</li>
14
+			<li>Avatars must not exceed <span class="tooltip" title="524,288 bytes">512 kiB</span> or be vertically longer than 600 pixels. Avatars may contain nudity and offensive imagery, but must not be pictures of Spiderman</li>
15
+			<li>Do not post our torrent files on other sites. Your personal passkey is embedded in every torrent file. The tracker will automatically disable your account if you share your torrent files with others (maybe). You will not get your account back (maybe). This does not prohibit you from sharing the content of the torrents on other sites, but this does prohibit you from sharing the torrent file itself (i.e. the file with a ".torrent" file extension).</li>
16
+			<li>Any torrent you are seeding to this tracker must have <em>only</em> <?=SITE_NAME?>'s tracker URL in it. Adding another BitTorrent tracker's URL will cause incorrect data to be sent to our tracker, and you will be disabled for cheating. Similarly, your client must have DHT and PEX (peer exchange) disabled for all <?=SITE_NAME?> torrents.</li>
17
+			<li>This is a BitTorrent site which promotes sharing amongst the community. If you are not willing to give back to the community what you take from it, this site is not for you. In other words, we expect you to have an acceptable share ratio. If you download a torrent, please seed the copy you have until there are sufficient people seeding the torrent before you stop.</li>
18
+			<li>Feel free to browse the site using proxies or Tor. We reserve the right to scrutinize your activity more than normal in these cases, but no harm, no foul. This includes VPNs with dynamic IP addresses.</li>
19
+			<li>Invites should be offered in the Invites forum and nowhere else.</li>
20
+			<li>Selling <?=SITE_NAME?> invites is strictly prohibited and will result in a permanent ban. Responding to public requests for invites may also jeopardize your account and the accounts of those you invite from a public request if the person you invite turns out to be a total shitfuck.</li>
21
+			<li>Buying <?=SITE_NAME?> invites is discouraged, but if you did buy an invite, tell us who the seller is and we'll let you keep your account. If you don't tell us and we find out, you're banned, kiddo.</li>
22
+			<li>Trading or selling your account is strictly prohibited. If you no longer want your account, send a <a href="staffpm.php">Staff PM</a> requesting that it be disabled. Do not give your account to some asshole.</li>
23
+			<li>You are completely responsible for the people you invite. If your invitees are caught cheating or selling invites, not only will they be banned, so will you (or we'll take away your invite privs). Be careful who you invite. Invites are a precious commodity.</li>
24
+			<li>Be careful when sharing an IP address or a computer with a friend if they have (or have had) an account. We don't really care where you log in from, but from then on, your accounts will be permanently linked because we're using Gazelle, and if one of you violates the rules, both accounts might be disabled along with any other accounts linked by IP address if we get confused. This rule applies to logging into the site.</li>
25
+			<li>Attempting to find a bug in the site code is absolutely fine. Misusing that knowledge is not, but we actively encourage users to try to find bugs and report them so they can be fixed. The discovery of significant bugs may result in a reward at the discretion of the staff. Do not be an asshole and try to flood the tracker or something and then come to us saying "lol I found bug gib reward"</li>
26
+			<li>We're a community. Working together is what makes this place what it is. There are well over a thousand new torrents uploaded every day (pfffff) and, sadly, the staff are only a little psychic. If you come across something that violates a rule, report it, and help us better organize the site for you.</li>
27
+			<li>We respect the wishes of other BitTorrent trackers that we agree with here, as we wish for them to do the same. Please refrain from posting full names or links to sites that do not want to be mentioned.</li>
28
+		</ol>
29
+<?
30
+	}
31
+
32
+	/**
33
+	 * Displays the site's rules for tags.
34
+	 *
35
+	 * @param boolean $OnUpload - whether it's being displayed on a torrent upload form
36
+	 */
37
+	public static function display_site_tag_rules($OnUpload = false) {
38
+		?>
39
+		<ul>
40
+			<li>Tags should be comma-separated, and you should use a period (".") to separate words inside a tag&#8202;&mdash;&#8202;e.g. "<strong class="important_text_alt">big.breasts</strong>".</li>
41
+
42
+			<li>There is a list of official tags <?=($OnUpload ? 'to the left of the text box' : 'on <a href="upload.php">the torrent upload page</a>')?>. Please use these tags instead of "unofficial" tags (e.g. use the official "<strong class="important_text_alt">paizuri</strong>" tag, instead of an unofficial "<strong class="important_text">titfuck</strong>" tag).</strong></li>
43
+
44
+			<li>Avoid using multiple synonymous tags. Using both "<strong class="important_text">pissing</strong>" and "<strong class="important_text_alt">urination</strong>" is redundant and stupid&#8202;&mdash;&#8202;just use the official "<strong class="important_text_alt">urination</strong>" tag.</li>
45
+
46
+			<li>Do not add useless tags.</li>
47
+
48
+			<li><strong>If one more person tags something "<strong class="important_text">hentai</strong>" I swear to god I'm gonna go nuclear on your worthless ass.</strong></li>
49
+
50
+			<li>Only tag information on the title itself&#8202;&mdash;&#8202;<strong>not the individual release</strong>. Tags such as "<strong class="important_text">mkv</strong>", "<strong class="important_text">windows</strong>", "<strong class="important_text">scan</strong>", "<strong class="important_text">from.dlsite</strong>", etc. are strictly forbidden. Remember that these tags will be used for other versions of the same title.</li>
51
+
52
+			<li>Derivative works may be tagged with the name of the parent series or characters within the work. For example, tags such as "<strong class="important_text_alt">touhou</strong>" or "<strong class="important_text_alt">iori.minase</strong>" may be acceptable if they are being used on derivative (parody) works. These kinds of tags should NOT be used if the series is primarily erotic in nature. Characters should be tagged with the form "<strong class="important_text_alt">surname.firstname</strong>" if applicable.</li>
53
+
54
+			<li><strong>Tags should reflect significant aspects of a torrent.</strong> Don't tag something with "<strong class="important_text">blowjob</strong>" if there's only 30 seconds of dick-sucking. However, certain tags may be acceptable, such as "<strong class="important_text_alt">stockings</strong>", even if the torrent in question isn't centered around that fetish. Be smart.</li>
55
+
56
+			<li><strong>Certain tags are strongly encouraged for appropriate uploads:</strong> "<strong class="important_text_alt">3d</strong>", "<strong class="important_text_alt">anthology</strong>", "<strong class="important_text_alt">yuri</strong>", "<strong class="important_text_alt">yaoi</strong>". People search for these kinds of things specifically, so tagging them properly will get you more snatches.</li>
57
+
58
+			<li>Tags for game genres such as "<strong class="important_text_alt">rpg</strong>", "<strong class="important_text_alt">visual.novel></strong>", or "<strong class="important_text_alt">nukige</strong>" are encouraged.</li>
59
+
60
+			<li><strong>Certain tags are <strong class="important_text">required</strong> for appropriate uploads:</strong>"<strong class="important_text_alt">lolicon</strong>", "<strong class="important_text_alt">shotacon</strong>", "<strong class="important_text_alt">toddlercon</strong>". Failure to use these tags may result in punishment.</li>
61
+
62
+			<li><strong>All uploads require a minimum of 5 tags.</strong></li>
63
+
64
+			<li><strong>You should be able to build up a list of tags using only the official tags <?=($OnUpload ? 'to the left of the text box' : 'on <a href="upload.php">the torrent upload page</a>')?>. If you are in any doubt about whether or not a tag is acceptable, do not add it.</strong></li>
65
+		</ul>
66
+<?
67
+	}
68
+
69
+	/**
70
+	 * Displays the site's rules for the forum
71
+	 *
72
+	 */
73
+	public static function display_forum_rules() {
74
+		?>
75
+		<ol>
76
+			<li>
77
+				Many forums have their own set of rules. Make sure you read and take note of these rules before you attempt to post in one of these forums.
78
+			</li>
79
+			<li>
80
+				Don't use all capital letters, excessive !!! (exclamation marks) or ??? (question marks) unless it's funny. Basically just don't type like a disgusting fucking normie.
81
+			</li>
82
+			<li>
83
+				No lame referral schemes. This includes freeipods.com, freepsps.com, or any other similar scheme in which the poster gets personal gain from users clicking a link. This shit is the WORST and you WILL be permanently banned on the spot, no questions asked you douche.
84
+			</li>
85
+			<li>
86
+				No asking for money for any reason whatsoever. We don't know or care about your friend who lost everything, or dying relative who wants to enjoy their last few moments alive by being given lots of money. The only ones allowed to shill around these parts are staff.
87
+			</li>
88
+			<li>
89
+				Do not inappropriately advertise your uploads. In special cases, it is acceptable to mention new uploads in an approved thread, but be sure to carefully read the thread's rules before posting. It is also acceptable to discuss releases you have uploaded when conversing about the title itself. Blatant attempts to advertise your uploads outside of the appropriate forums or threads may result in a warning or being called a faggot.
90
+			</li>
91
+			<li>
92
+				No posting requests in forums. There's a request link at the top of the page for a god damn reason.
93
+			</li>
94
+			<li>
95
+				No flaming unless they <i>really</i> deserve it. Feel free to use offensive language, but don't be confrontational for the sake of confrontation.
96
+			</li>
97
+			<li>
98
+				Don't point out or attack other members' share ratios. A higher ratio totally makes you better than other people, though.
99
+			</li>
100
+			<li>
101
+				Asking stupid questions will probably result in you being made fun of. A stupid question is one that you could have found the answer to yourself with a little research, or one that you're asking in the wrong place. If you do the basic research suggested (i.e., read the rules/wiki) or search the forums and don't find the answer to your question, then go ahead and ask. Staff and First Line Support (FLS) are not here to hand-feed you the answers you could have found on your own with a little bit of effort. Apply yourself. Put <i>some</i> effort in.
102
+			</li>
103
+			<li>
104
+				Be sure you read all the sticky threads in a forum before you post. Let's be honest, we both know you're not going to do this.
105
+			</li>
106
+			<li>
107
+				Use descriptive and specific subject lines. This helps others decide whether your particular words of "wisdom" relate to a topic they care about.
108
+			</li>
109
+			<li>
110
+				Don't post comments that don't add anything to the discussion. If you don't have anything valuable to say, don't say anything. When you're just cruising through a thread in a leisurely manner, it's not too annoying to read through a lot of "hear, hear"'s and "I agree"'s. But if you're actually trying to find information, it's a pain in the ass. So save those one-word responses for threads that have degenerated to the point where none but true aficionados are following them any more.
111
+				<p>
112
+					Or short: NO spamming, unless it's funny.
113
+				</p>
114
+			</li>
115
+			<li>
116
+				Refrain from quoting excessively. When quoting someone, use only the portion of the quote that is absolutely necessary. This includes quoting pictures! Don't quote the entire first post, either. I swear to god if I see you doing this it's over.
117
+			</li>
118
+			<li>
119
+				Feel free to request and discuss cracks for games and software. Links are fine, but if they're found to be malicious, the fire's gonna be under <i>your</i> ass.
120
+			</li>
121
+			<li>
122
+				Political or religious discussions are okay. These types of discussions lead to arguments and flaming users, which can be pretty funny to watch and gives staff a good excuse to ban people.
123
+			</li>
124
+			<li>
125
+				Don't waste other people's bandwidth by posting images with a large file size.
126
+			</li>
127
+			<li>
128
+				Be (a little) patient with newcomers. Once you have become an expert, it is easy to forget that you started out as a dumbass too.
129
+			</li>
130
+			<li>
131
+				Requesting invites to other sites is probably fine unless you're being obnoxious about it. Invites may be formally <strong>offered</strong> in the invite forum, and nowhere else.
132
+			</li>
133
+			<li>
134
+				No language other than English (and Japanese, when relevant) is permitted in the forums. If we can't understand it, we will assume you're calling our mothers whores.
135
+			</li>
136
+			<li>
137
+				Mature and graphic content on the forums and IRC is acceptable. In fact, it's expected. This is a porn site. Pictures of spiderman are prohibited.
138
+			</li>
139
+		</ol>
140
+<?
141
+	}
142
+
143
+	/**
144
+	 * Displays the site's rules for conversing on its IRC network
145
+	 *
146
+	 */
147
+	public static function display_irc_chat_rules() {
148
+		?>
149
+		<ol>
150
+			<li>Staff have the final decision. If a staff member says stop and you continue, expect to be called a faggot.</li>
151
+			<li>Be respectful to IRC Operators and Administrators. These people are site staff who do it for FREE. They are here for the benefit of all and to aid in conflict resolution. They enjoy Hot Pockets.</li>
152
+			<li>Do not link shock sites without a warning (unless it's funny). If in doubt, ask a staff member in <?=(BOT_HELP_CHAN)?> about it.</li>
153
+			<li>Excessive swearing will not get you kicked; practice being a sailor.</li>
154
+			<li>Do not leave Caps Lock enabled all the time. It gets annoying, and you will likely get yourself kicked.</li>
155
+			<li>No arguing. You can't win an argument <s>over the Internet</s> because you're probably a dumbass, so you're just wasting your time trying.</li>
156
+			<li>No opinions, especially related to race, religion, politics, etc are allowed. Failure to comply with a request to cease having an opinion WILL be considered a thoughtcrime and you WILL be reeducated.</li>
157
+			<li>Flooding is irritating and will warrant you a kick if it isn't funny enough or if an admin is cranky. This includes, but is not limited to, automatic "now playing" scripts, pasting large amounts of text, and multiple consecutive lines with no relevance to the conversation at hand.</li>
158
+			<li>Impersonation of other members&#8202;&mdash;&#8202;particularly staff members&#8202;&mdash;&#8202;will not go unpunished. If you are uncertain of a user's identity, check their vhost, and then continue to be uncertain of their identity.</li>
159
+			<li>Spamming is <b>strictly</b> forbidden unless it's funny. This includes, but is not limited to, personal sites, online auctions, and cans of blended meat.</li>
160
+			<li>Obsessive annoyance&#8202;&mdash;&#8202;both to other users and staff&#8202;&mdash;&#8202;will not be tolerated.</li>
161
+			<li>Do not PM, DCC, or Query anyone you don't know or have never talked to without asking first; this applies specifically to staff.</li>
162
+			<li>No language other than English is permitted in the official IRC channels. If you can't use a real language, just stay in your shithole country.</li>
163
+			<li>The offering, selling, trading, and giving away of invites to this or any other site on our IRC network is <strong>strictly whatever</strong>.</li>
164
+			<li>Bots are not permitted in official channels with the exception of <strong>#oppaitime-announce</strong> and <strong>#oppaitime-requests</strong>.</li>
165
+			<li>Any bots you have on IRC should authenticate with Udon using your own username and IRC key</li>
166
+			<li>Bots must identify themselves by setting the +B usermode on themselves.</li>
167
+			<li><strong>Read the channel topic before asking questions.</strong></li>
168
+		</ol>
169
+<?
170
+	}
171
+}

+ 427
- 0
classes/script_start.php View File

1
+<?php
2
+/*-- Script Start Class --------------------------------*/
3
+/*------------------------------------------------------*/
4
+/* This isnt really a class but a way to tie other      */
5
+/* classes and functions used all over the site to the  */
6
+/* page currently being displayed.                      */
7
+/*------------------------------------------------------*/
8
+/* The code that includes the main php files and		*/
9
+/* generates the page are at the bottom.				*/
10
+/*------------------------------------------------------*/
11
+/********************************************************/
12
+require 'config.php'; //The config contains all site wide configuration information
13
+//Deal with dumbasses
14
+if (isset($_REQUEST['info_hash']) && isset($_REQUEST['peer_id'])) {
15
+	die('d14:failure reason40:Invalid .torrent, try downloading again.e');
16
+}
17
+
18
+require(SERVER_ROOT.'/classes/proxies.class.php');
19
+
20
+// Get the user's actual IP address if they're proxied.
21
+// Or if cloudflare is used
22
+if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
23
+	$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP'];
24
+}
25
+if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])
26
+		&& proxyCheck($_SERVER['REMOTE_ADDR'])
27
+		&& filter_var($_SERVER['HTTP_X_FORWARDED_FOR'],
28
+				FILTER_VALIDATE_IP,
29
+				FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
30
+	$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
31
+}
32
+
33
+$SSL = true; #(isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443);
34
+if (!isset($argv) && !empty($_SERVER['HTTP_HOST'])) {
35
+//Skip this block if running from cli or if the browser is old and shitty
36
+	if (!$SSL && $_SERVER['HTTP_HOST'] == 'www.'.NONSSL_SITE_URL) {
37
+		header('Location: http://'.NONSSL_SITE_URL.$_SERVER['REQUEST_URI']); die();
38
+	}
39
+	if ($SSL && $_SERVER['HTTP_HOST'] == 'www.'.NONSSL_SITE_URL) {
40
+		header('Location: https://'.SSL_SITE_URL.$_SERVER['REQUEST_URI']); die();
41
+	}
42
+	if (SSL_SITE_URL != NONSSL_SITE_URL) {
43
+		if (!$SSL && $_SERVER['HTTP_HOST'] == SSL_SITE_URL) {
44
+			header('Location: https://'.SSL_SITE_URL.$_SERVER['REQUEST_URI']); die();
45
+		}
46
+		if ($SSL && $_SERVER['HTTP_HOST'] == NONSSL_SITE_URL) {
47
+			header('Location: https://'.SSL_SITE_URL.$_SERVER['REQUEST_URI']); die();
48
+		}
49
+	}
50
+	if ($_SERVER['HTTP_HOST'] == 'www.m.'.NONSSL_SITE_URL) {
51
+		header('Location: http://m.'.NONSSL_SITE_URL.$_SERVER['REQUEST_URI']); die();
52
+	}
53
+}
54
+
55
+
56
+
57
+$ScriptStartTime = microtime(true); //To track how long a page takes to create
58
+if (!defined('PHP_WINDOWS_VERSION_MAJOR')) {
59
+	$RUsage = getrusage();
60
+	$CPUTimeStart = $RUsage['ru_utime.tv_sec'] * 1000000 + $RUsage['ru_utime.tv_usec'];
61
+}
62
+ob_start(); //Start a buffer, mainly in case there is a mysql error
63
+
64
+
65
+require(SERVER_ROOT.'/classes/debug.class.php'); //Require the debug class
66
+require(SERVER_ROOT.'/classes/mysql.class.php'); //Require the database wrapper
67
+require(SERVER_ROOT.'/classes/cache.class.php'); //Require the caching class
68
+require(SERVER_ROOT.'/classes/encrypt.class.php'); //Require the encryption class
69
+require(SERVER_ROOT.'/classes/time.class.php'); //Require the time class
70
+require(SERVER_ROOT.'/classes/paranoia.class.php'); //Require the paranoia check_paranoia function
71
+require(SERVER_ROOT.'/classes/regex.php');
72
+require(SERVER_ROOT.'/classes/util.php');
73
+
74
+$Debug = new DEBUG;
75
+$Debug->handle_errors();
76
+$Debug->set_flag('Debug constructed');
77
+
78
+$DB = new DB_MYSQL;
79
+$Cache = new CACHE(MEMCACHED_SERVERS);
80
+$Enc = new CRYPT;
81
+
82
+// Autoload classes.
83
+require(SERVER_ROOT.'/classes/classloader.php');
84
+
85
+// Note: G::initialize is called twice.
86
+// This is necessary as the code inbetween (initialization of $LoggedUser) makes use of G::$DB and G::$Cache.
87
+// TODO: remove one of the calls once we're moving everything into that class
88
+G::initialize();
89
+
90
+//Begin browser identification
91
+
92
+$Browser = UserAgent::browser($_SERVER['HTTP_USER_AGENT']);
93
+$OperatingSystem = UserAgent::operating_system($_SERVER['HTTP_USER_AGENT']);
94
+
95
+$Debug->set_flag('start user handling');
96
+
97
+// Get classes
98
+// TODO: Remove these globals, replace by calls into Users
99
+list($Classes, $ClassLevels) = Users::get_classes();
100
+
101
+//-- Load user information
102
+// User info is broken up into many sections
103
+// Heavy - Things that the site never has to look at if the user isn't logged in (as opposed to things like the class, donor status, etc)
104
+// Light - Things that appear in format_user
105
+// Stats - Uploaded and downloaded - can be updated by a script if you want super speed
106
+// Session data - Information about the specific session
107
+// Enabled - if the user's enabled or not
108
+// Permissions
109
+
110
+if (isset($_COOKIE['session'])) {
111
+	$LoginCookie = $Enc->decrypt($_COOKIE['session']);
112
+}
113
+if (isset($LoginCookie)) {
114
+	list($SessionID, $LoggedUser['ID']) = explode('|~|', $Enc->decrypt($LoginCookie));
115
+	$LoggedUser['ID'] = (int)$LoggedUser['ID'];
116
+
117
+	$UserID = $LoggedUser['ID']; //TODO: UserID should not be LoggedUser
118
+
119
+	if (!$LoggedUser['ID'] || !$SessionID) {
120
+		logout();
121
+	}
122
+
123
+	$UserSessions = $Cache->get_value("users_sessions_$UserID");
124
+	if (!is_array($UserSessions)) {
125
+    $DB->query(
126
+		 "SELECT
127
+				SessionID,
128
+				Browser,
129
+        OperatingSystem,
130
+        IP, 
131
+			  LastUpdate
132
+			FROM users_sessions
133
+			WHERE UserID = '$UserID'
134
+				AND Active = 1
135
+			ORDER BY LastUpdate DESC");
136
+		$UserSessions = $DB->to_array('SessionID',MYSQLI_ASSOC);
137
+		$Cache->cache_value("users_sessions_$UserID", $UserSessions, 0);
138
+	}
139
+
140
+	if (!array_key_exists($SessionID, $UserSessions)) {
141
+		logout();
142
+	}
143
+
144
+	// Check if user is enabled
145
+	$Enabled = $Cache->get_value('enabled_'.$LoggedUser['ID']);
146
+	if ($Enabled === false) {
147
+		$DB->query("
148
+			SELECT Enabled
149
+			FROM users_main
150
+			WHERE ID = '$LoggedUser[ID]'");
151
+		list($Enabled) = $DB->next_record();
152
+		$Cache->cache_value('enabled_'.$LoggedUser['ID'], $Enabled, 0);
153
+	}
154
+	if ($Enabled == 2) {
155
+
156
+		logout();
157
+	}
158
+
159
+	// Up/Down stats
160
+	$UserStats = $Cache->get_value('user_stats_'.$LoggedUser['ID']);
161
+	if (!is_array($UserStats)) {
162
+		$DB->query("
163
+			SELECT Uploaded AS BytesUploaded, Downloaded AS BytesDownloaded, RequiredRatio
164
+			FROM users_main
165
+			WHERE ID = '$LoggedUser[ID]'");
166
+		$UserStats = $DB->next_record(MYSQLI_ASSOC);
167
+		$Cache->cache_value('user_stats_'.$LoggedUser['ID'], $UserStats, 3600);
168
+	}
169
+
170
+	// Get info such as username
171
+	$LightInfo = Users::user_info($LoggedUser['ID']);
172
+	$HeavyInfo = Users::user_heavy_info($LoggedUser['ID']);
173
+
174
+	// Create LoggedUser array
175
+	$LoggedUser = array_merge($HeavyInfo, $LightInfo, $UserStats);
176
+
177
+	$LoggedUser['RSS_Auth'] = md5($LoggedUser['ID'] . RSS_HASH . $LoggedUser['torrent_pass']);
178
+
179
+	// $LoggedUser['RatioWatch'] as a bool to disable things for users on Ratio Watch
180
+	$LoggedUser['RatioWatch'] = (
181
+		$LoggedUser['RatioWatchEnds'] != '0000-00-00 00:00:00'
182
+		&& time() < strtotime($LoggedUser['RatioWatchEnds'])
183
+		&& ($LoggedUser['BytesDownloaded'] * $LoggedUser['RequiredRatio']) > $LoggedUser['BytesUploaded']
184
+	);
185
+
186
+	// Load in the permissions
187
+	$LoggedUser['Permissions'] = Permissions::get_permissions_for_user($LoggedUser['ID'], $LoggedUser['CustomPermissions']);
188
+	$LoggedUser['Permissions']['MaxCollages'] += Donations::get_personal_collages($LoggedUser['ID']);
189
+
190
+	// Change necessary triggers in external components
191
+	$Cache->CanClear = check_perms('admin_clear_cache');
192
+
193
+	// Because we <3 our staff
194
+	if (check_perms('site_disable_ip_history')) {
195
+		$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
196
+	}
197
+
198
+	// Update LastUpdate every 10 minutes
199
+	if (strtotime($UserSessions[$SessionID]['LastUpdate']) + 600 < time()) {
200
+		$DB->query("
201
+			UPDATE users_main
202
+			SET LastAccess = '".sqltime()."'
203
+			WHERE ID = '$LoggedUser[ID]'");
204
+		$SessionQuery = 
205
+     "UPDATE users_sessions
206
+      SET ";
207
+    // Only update IP if we have an encryption key in memory
208
+    if (apc_exists('DBKEY')) {
209
+      $SessionQuery .= "IP = '".DBCrypt::encrypt($_SERVER['REMOTE_ADDR'])."', ";
210
+    }
211
+    $SessionQuery .=
212
+       "Browser = '$Browser',
213
+				OperatingSystem = '$OperatingSystem',
214
+				LastUpdate = '".sqltime()."'
215
+			WHERE UserID = '$LoggedUser[ID]'
216
+				AND SessionID = '".db_string($SessionID)."'";
217
+    $DB->query($SessionQuery);
218
+		$Cache->begin_transaction("users_sessions_$UserID");
219
+		$Cache->delete_row($SessionID);
220
+    $UsersSessionCache = array(
221
+				'SessionID' => $SessionID,
222
+				'Browser' => $Browser,
223
+				'OperatingSystem' => $OperatingSystem,
224
+        'IP' => ((apc_exists('DBKEY')) ? DBCrypt::encrypt($_SERVER['REMOTE_ADDR']) : $UserSessions[$SessionID]['IP']),
225
+        'LastUpdate' => sqltime() );
226
+		$Cache->insert_front($SessionID, $UsersSessionCache);
227
+		$Cache->commit_transaction(0);
228
+	}
229
+
230
+	// Notifications
231
+	if (isset($LoggedUser['Permissions']['site_torrents_notify'])) {
232
+		$LoggedUser['Notify'] = $Cache->get_value('notify_filters_'.$LoggedUser['ID']);
233
+		if (!is_array($LoggedUser['Notify'])) {
234
+			$DB->query("
235
+				SELECT ID, Label
236
+				FROM users_notify_filters
237
+				WHERE UserID = '$LoggedUser[ID]'");
238
+			$LoggedUser['Notify'] = $DB->to_array('ID');
239
+			$Cache->cache_value('notify_filters_'.$LoggedUser['ID'], $LoggedUser['Notify'], 2592000);
240
+		}
241
+	}
242
+
243
+	// We've never had to disable the wiki privs of anyone.
244
+	if ($LoggedUser['DisableWiki']) {
245
+		unset($LoggedUser['Permissions']['site_edit_wiki']);
246
+	}
247
+
248
+	// IP changed
249
+
250
+	if (apc_exists('DBKEY') && DBCrypt::decrypt($LoggedUser['IP']) != $_SERVER['REMOTE_ADDR'] && !check_perms('site_disable_ip_history')) {
251
+
252
+		if (Tools::site_ban_ip($_SERVER['REMOTE_ADDR'])) {
253
+			error('Your IP address has been banned.');
254
+		}
255
+
256
+		$CurIP = db_string($LoggedUser['IP']);
257
+		$NewIP = db_string($_SERVER['REMOTE_ADDR']);
258
+    $DB->query("
259
+      SELECT IP
260
+      FROM users_history_ips
261
+      WHERE EndTime IS NULL
262
+        AND UserID = '$LoggedUser[ID]'");
263
+    while (list($EncIP) = $DB->next_record()) {
264
+      if (DBCrypt::decrypt($EncIP) == $CurIP) {
265
+        $CurIP = $EncIP;
266
+        // CurIP is now the encrypted IP that was already in the database (for matching)
267
+        break;
268
+      }
269
+    }
270
+		$DB->query("
271
+			UPDATE users_history_ips
272
+			SET EndTime = '".sqltime()."'
273
+			WHERE EndTime IS NULL
274
+				AND UserID = '$LoggedUser[ID]'
275
+				AND IP = '$CurIP'");
276
+		$DB->query("
277
+			INSERT IGNORE INTO users_history_ips
278
+				(UserID, IP, StartTime)
279
+			VALUES
280
+				('$LoggedUser[ID]', '".DBCrypt::encrypt($NewIP)."', '".sqltime()."')");
281
+
282
+		$ipcc = Tools::geoip($NewIP);
283
+		$DB->query("
284
+			UPDATE users_main
285
+			SET IP = '".DBCrypt::encrypt($NewIP)."', ipcc = '$ipcc'
286
+			WHERE ID = '$LoggedUser[ID]'");
287
+		$Cache->begin_transaction('user_info_heavy_'.$LoggedUser['ID']);
288
+		$Cache->update_row(false, array('IP' => DBCrypt::encrypt($_SERVER['REMOTE_ADDR'])));
289
+		$Cache->commit_transaction(0);
290
+
291
+
292
+	}
293
+
294
+
295
+	// Get stylesheets
296
+	$Stylesheets = $Cache->get_value('stylesheets');
297
+	if (!is_array($Stylesheets)) {
298
+		$DB->query('
299
+			SELECT
300
+				ID,
301
+				LOWER(REPLACE(Name, " ", "_")) AS Name,
302
+				Name AS ProperName
303
+			FROM stylesheets');
304
+		$Stylesheets = $DB->to_array('ID', MYSQLI_BOTH);
305
+		$Cache->cache_value('stylesheets', $Stylesheets, 0);
306
+	}
307
+
308
+	//A9 TODO: Clean up this messy solution
309
+	$LoggedUser['StyleName'] = $Stylesheets[$LoggedUser['StyleID']]['Name'];
310
+
311
+	if (empty($LoggedUser['Username'])) {
312
+		logout(); // Ghost
313
+	}
314
+}
315
+G::initialize();
316
+$Debug->set_flag('end user handling');
317
+
318
+$Debug->set_flag('start function definitions');
319
+
320
+/**
321
+ * Log out the current session
322
+ */
323
+function logout() {
324
+	global $SessionID;
325
+	setcookie('session', '', time() - 60 * 60 * 24 * 365, '/', '', false);
326
+	setcookie('keeplogged', '', time() - 60 * 60 * 24 * 365, '/', '', false);
327
+	setcookie('session', '', time() - 60 * 60 * 24 * 365, '/', '', false);
328
+	if ($SessionID) {
329
+
330
+		G::$DB->query("
331
+			DELETE FROM users_sessions
332
+			WHERE UserID = '" . G::$LoggedUser['ID'] . "'
333
+				AND SessionID = '".db_string($SessionID)."'");
334
+
335
+		G::$Cache->begin_transaction('users_sessions_' . G::$LoggedUser['ID']);
336
+		G::$Cache->delete_row($SessionID);
337
+		G::$Cache->commit_transaction(0);
338
+	}
339
+	G::$Cache->delete_value('user_info_' . G::$LoggedUser['ID']);
340
+	G::$Cache->delete_value('user_stats_' . G::$LoggedUser['ID']);
341
+	G::$Cache->delete_value('user_info_heavy_' . G::$LoggedUser['ID']);
342
+
343
+	header('Location: login.php');
344
+
345
+	die();
346
+}
347
+
348
+function logout_all_sessions() {
349
+	$UserID = G::$LoggedUser['ID'];
350
+
351
+	G::$DB->query("
352
+		DELETE FROM users_sessions
353
+		WHERE UserID = '$UserID'");
354
+	
355
+	G::$Cache->delete_value('users_sessions_' . $UserID);
356
+	logout();
357
+}
358
+
359
+function enforce_login() {
360
+	global $SessionID;
361
+	if (!$SessionID || !G::$LoggedUser) {
362
+		setcookie('redirect', $_SERVER['REQUEST_URI'], time() + 60 * 30, '/', '', false);
363
+		logout();
364
+	}
365
+}
366
+
367
+/**
368
+ * Make sure $_GET['auth'] is the same as the user's authorization key
369
+ * Should be used for any user action that relies solely on GET.
370
+ *
371
+ * @param Are we using ajax?
372
+ * @return authorisation status. Prints an error message to LAB_CHAN on IRC on failure.
373
+ */
374
+function authorize($Ajax = false) {
375
+	if (empty($_REQUEST['auth']) || $_REQUEST['auth'] != G::$LoggedUser['AuthKey']) {
376
+		send_irc("PRIVMSG ".LAB_CHAN." :".G::$LoggedUser['Username']." just failed authorize on ".$_SERVER['REQUEST_URI'].(!empty($_SERVER['HTTP_REFERER']) ? " coming from ".$_SERVER['HTTP_REFERER'] : ""));
377
+		error('Invalid authorization key. Go back, refresh, and try again.', $Ajax);
378
+		return false;
379
+	}
380
+	return true;
381
+}
382
+
383
+$Debug->set_flag('ending function definitions');
384
+//Include /sections/*/index.php
385
+$Document = basename(parse_url($_SERVER['SCRIPT_FILENAME'], PHP_URL_PATH), '.php');
386
+if (!preg_match('/^[a-z0-9]+$/i', $Document)) {
387
+	error(404);
388
+}
389
+
390
+$StripPostKeys = array_fill_keys(array('password', 'cur_pass', 'new_pass_1', 'new_pass_2', 'verifypassword', 'confirm_password', 'ChangePassword', 'Password'), true);
391
+$Cache->cache_value('php_' . getmypid(), array(
392
+	'start' => sqltime(),
393
+	'document' => $Document,
394
+	'query' => $_SERVER['QUERY_STRING'],
395
+	'get' => $_GET,
396
+	'post' => array_diff_key($_POST, $StripPostKeys)), 600);
397
+
398
+// Locked account constant
399
+define('STAFF_LOCKED', 1);
400
+
401
+$AllowedPages = ['staffpm', 'ajax', 'locked', 'logout', 'login'];
402
+
403
+if (isset(G::$LoggedUser['LockedAccount']) && !in_array($Document, $AllowedPages)) {
404
+  require(SERVER_ROOT . '/sections/locked/index.php');
405
+} else {
406
+  require(SERVER_ROOT . '/sections/' . $Document . '/index.php');
407
+}
408
+
409
+$Debug->set_flag('completed module execution');
410
+
411
+/* Required in the absence of session_start() for providing that pages will change
412
+upon hit rather than being browser cached for changing content.
413
+
414
+Old versions of Internet Explorer choke when downloading binary files over HTTPS with disabled cache.
415
+Define the following constant in files that handle file downloads */
416
+if (!defined('SKIP_NO_CACHE_HEADERS')) {
417
+	header('Cache-Control: no-cache, must-revalidate, post-check=0, pre-check=0');
418
+	header('Pragma: no-cache');
419
+}
420
+
421
+//Flush to user
422
+ob_end_flush();
423
+
424
+$Debug->set_flag('set headers and send to user');
425
+
426
+//Attribute profiling
427
+$Debug->profile();

+ 269
- 0
classes/sitehistory.class.php View File

1
+<?
2
+
3
+class SiteHistory {
4
+	private static $Categories = array(1 => "Code", "Event", "Milestone", "Policy", "Release", "Staff Change");
5
+	private static $SubCategories = array(1 => "Announcement", "Blog Post", "Change Log", "Forum Post", "Wiki", "Other", "External Source");
6
+	private static $Tags = array(
7
+								"api",
8
+								"celebration",
9
+								"class.primary",
10
+								"class.secondary",
11
+								"collage",
12
+								"community",
13
+								"conclusion",
14
+								"contest",
15
+								"design",
16
+								"donate",
17
+								"editing",
18
+								"editorial",
19
+								"feature",
20
+								"featured.article",
21
+								"featured.album",
22
+								"featured.product",
23
+								"finances",
24
+								"format",
25
+								"forum",
26
+								"freeleech",
27
+								"freeleech.tokens",
28
+								"gazelle",
29
+								"hierarchy",
30
+								"inbox",
31
+								"infrastructure",
32
+								"interview",
33
+								"irc",
34
+								"log",
35
+								"neutral.leech",
36
+								"notifications",
37
+								"ocelot",
38
+								"paranoia",
39
+								"picks.guest",
40
+								"picks.staff",
41
+								"promotion",
42
+								"ratio",
43
+								"record",
44
+								"report",
45
+								"request",
46
+								"requirement",
47
+								"retirement",
48
+								"rippy",
49
+								"search",
50
+								"settings",
51
+								"start",
52
+								"stats",
53
+								"store",
54
+								"stylesheet",
55
+								"tagging",
56
+								"transcode",
57
+								"toolbox",
58
+								"top.10",
59
+								"torrent",
60
+								"torrent.group",
61
+								"upload",
62
+								"vanity.house",
63
+								"voting",
64
+								"whitelist",
65
+								"wiki");
66
+
67
+	public static function get_months() {
68
+		$Results = G::$Cache->get_value("site_history_months");
69
+		if (!$Results) {
70
+			$QueryID = G::$DB->get_query_id();
71
+			G::$DB->query("
72
+					SELECT DISTINCT
73
+						YEAR(DATE) AS Year, MONTH(Date) AS Month, MONTHNAME(Date) AS MonthName
74
+					FROM site_history
75
+					ORDER BY Date DESC");
76
+			$Results = G::$DB->to_array();
77
+			G::$DB->set_query_id($QueryID);
78
+			G::$Cache->cache_value("site_history_months", $Results, 0);
79
+		}
80
+		return $Results;
81
+	}
82
+
83
+	public static function get_event($ID) {
84
+		if (!empty($ID)) {
85
+			$QueryID = G::$DB->get_query_id();
86
+			G::$DB->query("
87
+					SELECT
88
+						ID, Title, Url, Category, SubCategory, Tags, Body, AddedBy, Date
89
+					FROM site_history
90
+					WHERE ID = '$ID'
91
+					ORDER BY Date DESC");
92
+			$Event = G::$DB->next_record();
93
+			G::$DB->set_query_id($QueryID);
94
+			return $Event;
95
+		}
96
+	}
97
+
98
+	public static function get_latest_events($Limit) {
99
+		self::get_events(null, null, null, null, null, null, $Limit);
100
+	}
101
+
102
+	public static function get_events($Month, $Year, $Title, $Category, $SubCategory, $Tags, $Limit) {
103
+		$Month = (int)$Month;
104
+		$Year = (int)$Year;
105
+		$Title = db_string($Title);
106
+		$Category = (int)$Category;
107
+		$SubCategory = (int)$SubCategory;
108
+		$Tags = db_string($Tags);
109
+		$Limit = (int)$Limit;
110
+		$Where = array();
111
+		if (!empty($Month)) {
112
+			$Where[] = " MONTH(Date) = '$Month' ";
113
+		}
114
+		if (!empty($Year)) {
115
+			$Where[] = " YEAR(Date) = '$Year' ";
116
+		}
117
+		if (!empty($Title)) {
118
+			$Where[] = " Title LIKE '%$Title%' ";
119
+		}
120
+		if (!empty($Category)) {
121
+			$Where[] = " Category = '$Category '";
122
+		}
123
+		if (!empty($SubCategory)) {
124
+			$Where[] = " SubCategory = '$SubCategory '";
125
+		}
126
+		if (!empty($Tags)) {
127
+			$Tags = explode(',', $Tags);
128
+			$Or = '(';
129
+			foreach ($Tags as $Tag) {
130
+				$Tag = trim($Tag);
131
+				$Or .= " Tags LIKE '%$Tag%' OR ";
132
+			}
133
+			if (strlen($Or) > 1) {
134
+				$Or = rtrim($Or, 'OR ');
135
+				$Or .= ')';
136
+				$Where[] = $Or;
137
+			}
138
+		}
139
+		if (!empty($Limit)) {
140
+			$Limit = " LIMIT $Limit";
141
+		} else {
142
+			$Limit = '';
143
+		}
144
+		if (count($Where) > 0) {
145
+			$Query = ' WHERE ' . implode('AND', $Where);
146
+		} else {
147
+			$Query = '';
148
+		}
149
+
150
+		$QueryID = G::$DB->get_query_id();
151
+		G::$DB->query("
152
+				SELECT
153
+					ID, Title, Url, Category, SubCategory, Tags, Body, AddedBy, Date
154
+				FROM site_history
155
+				$Query
156
+				ORDER BY Date DESC
157
+				$Limit");
158
+		$Events = G::$DB->to_array();
159
+		G::$DB->set_query_id($QueryID);
160
+		return $Events;
161
+	}
162
+
163
+	public static function add_event($Date, $Title, $Link, $Category, $SubCategory, $Tags, $Body, $UserID) {
164
+		if (empty($Date)) {
165
+			$Date = sqltime();
166
+		} else {
167
+			list($Y, $M, $D) = explode('-', $Date);
168
+			if (!checkdate($M, $D, $Y)) {
169
+				error("Error");
170
+			}
171
+		}
172
+		$Title = db_string($Title);
173
+		$Link = db_string($Link);
174
+		$Category = (int)$Category;
175
+		$SubCategory = (int)$SubCategory;
176
+		$Tags = db_string(strtolower((preg_replace('/\s+/', '', $Tags))));
177
+		$ExplodedTags = explode(',', $Tags);
178
+		foreach ($ExplodedTags as $Tag) {
179
+			if (!in_array($Tag, self::get_tags())) {
180
+				error("Invalid tag");
181
+			}
182
+		}
183
+		$Body = db_string($Body);
184
+		$UserID = (int)$UserID;
185
+
186
+		if (empty($Title) || empty($Category) || empty($SubCategory)) {
187
+			error("Error");
188
+		}
189
+
190
+		$QueryID = G::$DB->get_query_id();
191
+		G::$DB->query("
192
+				INSERT INTO site_history
193
+					(Title, Url, Category, SubCategory, Tags, Body, AddedBy, Date)
194
+				VALUES
195
+					('$Title', '$Link', '$Category', '$SubCategory', '$Tags', '$Body', '$UserID', '$Date')");
196
+		G::$DB->set_query_id($QueryID);
197
+		G::$Cache->delete_value("site_history_months");
198
+	}
199
+
200
+	public static function update_event($ID, $Date, $Title, $Link, $Category, $SubCategory, $Tags, $Body, $UserID) {
201
+		if (empty($Date)) {
202
+			$Date = sqltime();
203
+		} else {
204
+			$Date = db_string($Date);
205
+			list($Y, $M, $D) = explode('-', $Date);
206
+			if (!checkdate($M, $D, $Y)) {
207
+				error("Error");
208
+			}
209
+		}
210
+		$ID = (int)$ID;
211
+		$Title = db_string($Title);
212
+		$Link = db_string($Link);
213
+		$Category = (int)$Category;
214
+		$SubCategory = (int)$SubCategory;
215
+		$Tags = db_string(strtolower((preg_replace('/\s+/', '', $Tags))));
216
+		$ExplodedTags = explode(",", $Tags);
217
+		foreach ($ExplodedTags as $Tag) {
218
+			if (!in_array($Tag, self::get_tags())) {
219
+				error("Invalid tag");
220
+			}
221
+		}
222
+		$Body = db_string($Body);
223
+		$UserID = (int)$UserID;
224
+
225
+		if (empty($ID) || empty($Title) || empty($Category) || empty($SubCategory)) {
226
+			error("Error");
227
+		}
228
+
229
+		$QueryID = G::$DB->get_query_id();
230
+		G::$DB->query("
231
+				UPDATE site_history
232
+				SET
233
+					Title = '$Title',
234
+					Url = '$Link',
235
+					Category = '$Category',
236
+					SubCategory = '$SubCategory',
237
+					Tags = '$Tags',
238
+					Body = '$Body',
239
+					AddedBy = '$UserID',
240
+					Date = '$Date'
241
+				WHERE ID = '$ID'");
242
+		G::$DB->set_query_id($QueryID);
243
+		G::$Cache->delete_value("site_history_months");
244
+	}
245
+
246
+	public static function delete_event($ID) {
247
+		if (!is_numeric($ID)) {
248
+			error(404);
249
+		}
250
+		$QueryID = G::$DB->get_query_id();
251
+		G::$DB->query("
252
+				DELETE FROM site_history
253
+				WHERE ID = '$ID'");
254
+		G::$DB->set_query_id($QueryID);
255
+		G::$Cache->delete_value("site_history_months");
256
+	}
257
+
258
+	public static function get_categories() {
259
+		return self::$Categories;
260
+	}
261
+
262
+	public static function get_sub_categories() {
263
+		return self::$SubCategories;
264
+	}
265
+
266
+	public static function get_tags() {
267
+		return self::$Tags;
268
+	}
269
+}

+ 226
- 0
classes/sitehistoryview.class.php View File

1
+<?
2
+
3
+class SiteHistoryView {
4
+
5
+	public static function render_linkbox() {
6
+		if (check_perms('users_mod')
7
+			) {
8
+?>
9
+	<div class="linkbox">
10
+		<a href="sitehistory.php?action=edit" class="brackets">Create new event</a>
11
+	</div>
12
+<?
13
+		}
14
+	}
15
+
16
+	public static function render_events($Events) {
17
+		$Categories = SiteHistory::get_categories();
18
+		$SubCategories = SiteHistory::get_sub_categories();
19
+		$CanEdit = check_perms('users_mod') ;
20
+		foreach ($Events as $Event) {
21
+?>
22
+			<div class="box">
23
+				<div class="head colhead_dark">
24
+					<div class="title">
25
+<?			if ($CanEdit) { ?>
26
+						<a class="brackets" href="sitehistory.php?action=edit&amp;id=<?=$Event['ID']?>">Edit</a>
27
+<?			} ?>
28
+
29
+						<?=date('F d, Y', strtotime($Event['Date']));?>
30
+							-
31
+						<a href="sitehistory.php?action=search&amp;category=<?=$Event['Category']?>" class="brackets"><?=$Categories[$Event['Category']]?></a>
32
+						<a href="sitehistory.php?action=search&amp;subcategory=<?=$Event['SubCategory']?>" class="brackets"><?=$SubCategories[$Event['SubCategory']]?></a>
33
+
34
+<?			if (!empty($Event['Url'])) { ?>
35
+						<a href="<?=$Event['Url']?>"><?=$Event['Title']?></a>
36
+<?			} else { ?>
37
+						<?=$Event['Title']?>
38
+<?			} ?>
39
+					</div>
40
+					<div class="tags">
41
+						<? self::render_tags($Event['Tags'])?>
42
+					</div>
43
+					</div>
44
+<?			if (!empty($Event['Body'])) { ?>
45
+					<div class="body">
46
+						<?=Text::full_format($Event['Body'])?>
47
+					</div>
48
+<?			} ?>
49
+			</div>
50
+<?
51
+		}
52
+	}
53
+
54
+	private static function render_tags($Tags) {
55
+		$Tags = explode(',', $Tags);
56
+		natcasesort($Tags);
57
+		$FormattedTags = '';
58
+		foreach ($Tags as $Tag) {
59
+			$FormattedTags .= "<a href=\"sitehistory.php?action=search&amp;tags=$Tag\">$Tag" . "</a>, ";
60
+		}
61
+		echo rtrim($FormattedTags, ', ');
62
+	}
63
+
64
+	public static function render_months($Months) { ?>
65
+		<div class="box">
66
+			<div class="head">Calendar</div>
67
+			<div class="pad">
68
+<?
69
+		$Year = "";
70
+		foreach ($Months as $Month) {
71
+			if ($Month['Year'] != $Year) {
72
+				$Year = $Month['Year'];
73
+				echo "<h2>$Year</h2>";
74
+			}
75
+?>
76
+				<a style="margin-left: 5px;" href="sitehistory.php?month=<?=$Month['Month']?>&amp;year=<?=$Month['Year']?>"><?=$Month['MonthName']?></a>
77
+<?		} ?>
78
+			</div>
79
+		</div>
80
+<?
81
+	}
82
+
83
+	public static function render_search() { ?>
84
+			<div class="box">
85
+				<div class="head">Search</div>
86
+				<div class="pad">
87
+					<form class="search_form" action="sitehistory.php" method="get">
88
+						<input type="hidden" name="action" value="search" />
89
+						<input type="text" id="title" name="title" size="20" placeholder="Title" />
90
+						<br /><br/>
91
+						<input type="text" id="tags" name="tags" size="20" placeholder="Comma-separated tags" />
92
+						<br /><br/>
93
+						<select name="category" id="category">
94
+							<option value="0">Choose a category</option>
95
+<?
96
+			$Categories = SiteHistory::get_categories();
97
+			foreach ($Categories as $Key => $Value) {
98
+?>
99
+							<option<?=$Key == $Event['Category'] ? ' selected="selected"' : ''?> value="<?=$Key?>"><?=$Value?></option>
100
+<?			} ?>
101
+						</select>
102
+						<br /><br/>
103
+						<select name="subcategory">
104
+							<option value="0">Choose a subcategory</option>
105
+<?
106
+			$SubCategories = SiteHistory::get_sub_categories();
107
+			foreach ($SubCategories as $Key => $Value) {
108
+?>
109
+							<option<?=$Key == $Event['SubCategory'] ? ' selected="selected"' : ''?> value="<?=$Key?>"><?=$Value?></option>
110
+<?			} ?>
111
+						</select>
112
+						<br /><br/>
113
+						<input value="Search" type="submit" />
114
+					</form>
115
+				</div>
116
+			</div>
117
+<?	}
118
+
119
+	public static function render_edit_form($Event) { ?>
120
+		<form id="event_form" method="post" action="">
121
+<?		if ($Event) { ?>
122
+			<input type="hidden" name="action" value="take_edit" />
123
+			<input type="hidden" name="id" value="<?=$Event['ID']?>" />
124
+<?		} else { ?>
125
+			<input type="hidden" name="action" value="take_create" />
126
+<?		} ?>
127
+			<input type="hidden" name="auth" value="<?=G::$LoggedUser['AuthKey']?>" />
128
+			<table cellpadding="6" cellspacing="1" border="0" class="layout border" width="100%">
129
+				<tr>
130
+					<td class="label">Title:</td>
131
+					<td>
132
+						<input type="text" id="title" name="title" size="50" class="required" value="<?=$Event['Title']?>" />
133
+					</td>
134
+				</tr>
135
+				<tr>
136
+					<td class="label">Link:</td>
137
+					<td>
138
+						<input type="text" id="url" name="url" size="50" value="<?=$Event['Url']?>" />
139
+					</td>
140
+				</tr>
141
+				<tr>
142
+					<td class="label">Date:</td>
143
+					<td>
144
+						<input type="date" id="date" name="date" class="required"<?=$Event ? ' value="' . date('Y-m-d', strtotime($Event['Date'])) . '"' : ''?> />
145
+					</td>
146
+				</tr>
147
+				<tr>
148
+					<td class="label">Category:</td>
149
+					<td>
150
+						<select id="category" name="category" class="required">
151
+							<option value="0">Choose a category</option>
152
+<?
153
+		$Categories = SiteHistory::get_categories();
154
+		foreach ($Categories as $Key => $Value) {
155
+?>
156
+							<option<?=$Key == $Event['Category'] ? ' selected="selected"' : ''?> value="<?=$Key?>"><?=$Value?></option>
157
+<?		} ?>
158
+						</select>
159
+					</td>
160
+				</tr>
161
+				<tr>
162
+					<td class="label">Subcategory:</td>
163
+					<td>
164
+						<select id="category" name="sub_category" class="required">
165
+							<option value="0">Choose a subcategory</option>
166
+<?		$SubCategories = SiteHistory::get_sub_categories();
167
+		foreach ($SubCategories as $Key => $Value) { ?>
168
+							<option<?=$Key == $Event['SubCategory'] ? ' selected="selected"' : ''?> value="<?=$Key?>"><?=$Value?></option>
169
+<?		} ?>
170
+						</select>
171
+					</td>
172
+				</tr>
173
+				<tr>
174
+					<td class="label">Tags:</td>
175
+					<td>
176
+						<input type="text" id="tags" name="tags" placeholder="Comma-separated tags; use periods/dots for spaces" size="50" value="<?=$Event['Tags']?>" />
177
+						<select id="tag_list">
178
+							<option>Choose tags</option>
179
+<?
180
+		$Tags = SiteHistory::get_tags();
181
+		foreach ($Tags as $Tag) {
182
+?>
183
+							<option><?=$Tag?></option>
184
+<?		} ?>
185
+						</select>
186
+					</td>
187
+				</tr>
188
+				<tr>
189
+					<td class="label">Body:</td>
190
+					<td>
191
+						<textarea id="body" name="body" cols="90" rows="8" tabindex="1" onkeyup="resize('body');"><?=$Event['Body']?></textarea>
192
+					</td>
193
+				</tr>
194
+			</table>
195
+			<input type="submit" name="submit" value="Submit" />
196
+<?		if ($Event) { ?>
197
+			<input type="submit" name="delete" value="Delete" />
198
+<?		} ?>
199
+		</form>
200
+<?
201
+	}
202
+
203
+	public static function render_recent_sidebar($Events) { ?>
204
+		<div class="box">
205
+			<div class="head colhead_dark">
206
+				<strong><a href="sitehistory.php">Latest site history</a></strong>
207
+			</div>
208
+			<ul class="stats nobullet">
209
+<?
210
+		$Categories = SiteHistory::get_categories();
211
+		foreach ($Events as $Event) {
212
+?>
213
+				<li>
214
+					<a href="sitehistory.php?action=search&amp;category=<?=$Event['Category']?>" class="brackets"><?=$Categories[$Event['Category']]?></a>
215
+<?			if (!empty($Event['Url'])) { ?>
216
+					<a href="<?=$Event['Url']?>"><?=Format::cut_string($Event['Title'], 20)?></a>
217
+<?			} else { ?>
218
+					<?=Format::cut_string($Event['Title'], 20)?>
219
+<?			} ?>
220
+				</li>
221
+<?		} ?>
222
+			</ul>
223
+		</div>
224
+<?
225
+	}
226
+}

+ 25
- 0
classes/siteoptions.class.php View File

1
+<?
2
+/*
3
+ * Class to manage site options
4
+ */
5
+class SiteOptions {
6
+
7
+  /*
8
+   * Get a site option
9
+   *
10
+   * @param string $Name The option name
11
+   * @param string $DefaultValue The value to default to if the name can't be found in the cache
12
+   */
13
+  public static function getSiteOption($Name, $DefaultValue) {
14
+    $Value = G::$Cache->get_value('site_option_' . $Name);
15
+
16
+    if ($Value === false) {
17
+      G::$DB->query("SELECT Value FROM site_options WHERE Name = '" . db_string($Name) . "'");
18
+      if (G::$DB->has_results()) {
19
+        list($Value) = G::$DB->next_record();
20
+        G::$Cache->cache_value('site_option_' . $Name, $Value);
21
+      }
22
+    }
23
+    return ($Value === false ? $DefaultValue : $Value);
24
+  }
25
+}

+ 0
- 0
classes/sphinxql.class.php View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save