Browse Source

Initial commit

spaghetti 2 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

@@ -0,0 +1 @@
1
+<? require('classes/script_start.php'); ?>

+ 1
- 0
announce.php View File

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

+ 3
- 0
artist.php View File

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

+ 3
- 0
better.php View File

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

+ 1
- 0
blog.php View File

@@ -0,0 +1 @@
1
+<? require('classes/script_start.php');

+ 3
- 0
bookmarks.php View File

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

+ 1
- 0
browse.php View File

@@ -0,0 +1 @@
1
+<? header('Location: torrents.php');

+ 1
- 0
captcha.php View File

@@ -0,0 +1 @@
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

@@ -0,0 +1,2 @@
1
+<? require("classes/script_start.php");
2
+

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


+ 256
- 0
classes/NMA_API.php View File

@@ -0,0 +1,256 @@
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

@@ -0,0 +1,3 @@
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

@@ -0,0 +1,209 @@
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

@@ -0,0 +1,385 @@
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

@@ -0,0 +1,361 @@
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

@@ -0,0 +1,145 @@
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

@@ -0,0 +1,96 @@
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

@@ -0,0 +1,184 @@
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

@@ -0,0 +1,143 @@
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

@@ -0,0 +1,32 @@
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

@@ -0,0 +1,99 @@
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

@@ -0,0 +1,394 @@
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

@@ -0,0 +1,140 @@
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

@@ -0,0 +1,119 @@
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