diff --git a/classes/config.template b/classes/config.template index 71080bb0..e5623846 100644 --- a/classes/config.template +++ b/classes/config.template @@ -104,6 +104,11 @@ define('LOG_ENTRIES_PER_PAGE', 50); // Cache catalogues define('THREAD_CATALOGUE', 500); // Limit to THREAD_CATALOGUE posts per cache key. +// Email delivery method and information +define('EMAIL_DELIVERY_TYPE', ''); // should be either 'mailgun' to use mailgun services or 'local' to use a local SMTP server or relay +define('MAILGUN_API_KEY', ''); +define('MAILGUN_API_URL', ''); + // IRC settings define('BOT_NICK', ''); define('BOT_SERVER', ''); // IRC server address. Used for onsite chat tool. diff --git a/classes/irc.class.php b/classes/irc.class.php index d6e1af41..6debcea8 100644 --- a/classes/irc.class.php +++ b/classes/irc.class.php @@ -180,46 +180,6 @@ protected function listen() { } } - if (preg_match("/:([^!]+)![^\s]* QUIT.* /", $this->Data, $Nick)) { - if (isset($this->Identified[$Nick[1]])) { - unset($this->Identified[$Nick[1]]); - } - if (isset($this->DisabledUsers[$Nick[1]])) { - if ($this->DisabledUsers[$Nick[1]]['UserID'] != 0) { - G::$DB->query(" - DELETE FROM disable_list - WHERE Nick = '$Nick[1]'"); - G::$Cache->decrement_value('num_disablees'); - } - unset($this->DisabledUsers[$Nick[1]]); - } - } - - if (preg_match("/:([^!]+)![^\s]* PART ".BOT_DISABLED_CHAN.'/', $this->Data, $Nick)) { - if (isset($this->DisabledUsers[$Nick[1]])) { - if ($this->DisabledUsers[$Nick[1]]['UserID'] != 0) { - G::$DB->query(" - DELETE FROM disable_list - WHERE Nick = '$Nick[1]'"); - G::$Cache->decrement_value('num_disablees'); - } - unset($this->DisabledUsers[$Nick[1]]); - } - } - - if (preg_match("/:([^!]+)![^\s]* KICK ".BOT_DISABLED_CHAN.'.* /', $this->Data, $Nick)) { - $Nick = explode(' ', $Nick[0]); - if (isset($this->DisabledUsers[$Nick[3]])) { - if ($this->DisabledUsers[$Nick[3]]['UserID'] != 0) { - G::$DB->query(" - DELETE FROM disable_list - WHERE Nick = '$Nick[3]'"); - G::$Cache->decrement_value('num_disablees'); - } - unset($this->DisabledUsers[$Nick[3]]); - } - } - if (preg_match('/End of message of the day./', $this->Data)) { $this->connect_events(); } diff --git a/classes/misc.class.php b/classes/misc.class.php index fb104ef4..fa2f4eb4 100644 --- a/classes/misc.class.php +++ b/classes/misc.class.php @@ -1,24 +1,67 @@ '."\r\n"; - $Headers .= 'Reply-To: '.$From.'@'.NONSSL_SITE_URL."\r\n"; - $Headers .= 'X-Mailer: Project Gazelle'."\r\n"; - $Headers .= 'Message-Id: <'.Users::make_secret().'@'.NONSSL_SITE_URL.">\r\n"; - $Headers .= 'X-Priority: 3'."\r\n"; - mail($To, $Subject, $Body, $Headers, "-f $From@".NONSSL_SITE_URL); - } + /** + * Send an email. + * + * We can do this one of two ways - either using MailGun or with PHP's mail function. + * Checks for EMAIL_DELIVERY_TYPE and then proceeds as directed to send e-mail. + * + * @param string $To the email address to send it to. + * @param string $Subject + * @param string $Body + * @param string $From The user part of the user@NONSSL_SITE_URL email address. + * @param string $ContentType text/plain or text/html + */ + + public static function send_email($To, $Subject, $Body, $From, $ContentType) { + + switch (EMAIL_DELIVERY_TYPE) { + case 'local': + // remove the next line if you want to send HTML email from some places... + $ContentType='text/plain'; + $Headers = 'MIME-Version: 1.0'."\r\n"; + $Headers .= 'Content-type: '.$ContentType.'; charset=iso-8859-1'."\r\n"; + $Headers .= 'From: '.SITE_NAME.' <'.$From.'@'.NONSSL_SITE_URL.'>'."\r\n"; + $Headers .= 'Reply-To: '.$From.'@'.NONSSL_SITE_URL."\r\n"; + $Headers .= 'X-Mailer: Project Gazelle'."\r\n"; + $Headers .= 'Message-Id: <'.Users::make_secret().'@'.NONSSL_SITE_URL.">\r\n"; + $Headers .= 'X-Priority: 3'."\r\n"; + mail($To, $Subject, $Body, $Headers, "-f $From@".NONSSL_SITE_URL); + break; + + case 'mailgun': + // set up our message first + $From .= '@'.NONSSL_SITE_URL; + $OutgoingEmail = array( + 'from' => $From, + 'to' => $To, + 'h:Reply-To' => $From, + 'subject' => $Subject, + 'text' => $Body); + // now let's POST it to mailgun + $Curl = curl_init(); + curl_setopt($Curl, CURLOPT_URL, MAILGUN_API_URL); + curl_setopt($Curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($Curl, CURLOPT_USERPWD, 'api:'.MAILGUN_API_KEY); + curl_setopt($Curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($Curl, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($Curl, CURLOPT_POST, true); + curl_setopt($Curl, CURLOPT_POSTFIELDS, $OutgoingEmail); + + $RequestResult = curl_exec($Curl); + $RequestStatusCode = curl_getinfo($Curl, CURLINFO_HTTP_CODE); + curl_close($Curl); + // alert on failed emails + if ($RequestStatusCode != 200) { + send_irc('PRIVMSG '.STATUS_CHAN." !dev email failed to $To with error message $RequestResult"); + } + break; + + default: + die('You have either not configured an email delivery method in config.php or your value is incorrect.'); + break; + } + } /** diff --git a/classes/sphinxql.class.php b/classes/sphinxql.class.php index 51f9b888..a4cb7038 100644 --- a/classes/sphinxql.class.php +++ b/classes/sphinxql.class.php @@ -110,12 +110,13 @@ public function error($Msg, $Halt = false) { /** * Escape special characters before sending them to the Sphinx server. * Two escapes needed because the first one is eaten up by the mysql driver. + * Lowercase ASCII characters because some Sphinx operators are all caps words. * * @param string $String string to escape * @return escaped string */ public static function sph_escape_string($String) { - return strtr($String, array( + return strtr(strtolower($String), array( '('=>'\\\\(', ')'=>'\\\\)', '|'=>'\\\\|', diff --git a/classes/sphinxqlquery.class.php b/classes/sphinxqlquery.class.php index b6858193..b03f8e91 100644 --- a/classes/sphinxqlquery.class.php +++ b/classes/sphinxqlquery.class.php @@ -30,7 +30,7 @@ public function __construct($Server = SPHINXQL_HOST, $Port = SPHINXQL_PORT, $Soc * Specify what data the Sphinx query is supposed to return * * @param string $Fields Attributes and expressions - * @return current Sphinxql query object + * @return current SphinxqlQuery object */ public function select($Fields) { $this->Select = $Fields; @@ -41,7 +41,7 @@ public function select($Fields) { * Specify the indexes to use in the search * * @param string $Indexes comma separated list of indexes - * @return current Sphinxql query object + * @return current SphinxqlQuery object */ public function from($Indexes) { $this->Indexes = $Indexes; @@ -54,7 +54,7 @@ public function from($Indexes) { * @param string $Attribute attribute which the filter will apply to * @param mixed $Values scalar or array of numerical values. Array uses boolean OR in query condition * @param bool $Exclude whether to exclude or include matching documents. Default mode is to include matches - * @return current Sphinxql query object + * @return current SphinxqlQuery object */ public function where($Attribute, $Values, $Exclude = false) { if (empty($Attribute) || !isset($Values)) { @@ -95,7 +95,7 @@ public function where($Attribute, $Values, $Exclude = false) { * @param string $Attribute attribute which the filter will apply to * @param array $Value upper limit for matches * @param bool $Inclusive whether to use <= or < - * @return current Sphinxql query object + * @return current SphinxqlQuery object */ public function where_lt($Attribute, $Value, $Inclusive = false) { if (empty($Attribute) || !isset($Value) || !is_number($Value)) { @@ -112,7 +112,7 @@ public function where_lt($Attribute, $Value, $Inclusive = false) { * @param string $Attribute attribute which the filter will apply to * @param array $Value lower limit for matches * @param bool $Inclusive whether to use >= or > - * @return current Sphinxql query object + * @return current SphinxqlQuery object */ public function where_gt($Attribute, $Value, $Inclusive = false) { if (empty($Attribute) || !isset($Value) || !is_number($Value)) { @@ -128,7 +128,7 @@ public function where_gt($Attribute, $Value, $Inclusive = false) { * * @param string $Attribute attribute which the filter will apply to * @param array $Values pair of numerical values that defines the filter range - * @return current Sphinxql query object + * @return current SphinxqlQuery object */ public function where_between($Attribute, $Values) { if (empty($Attribute) || empty($Values) || count($Values) != 2 || !is_number($Values[0]) || !is_number($Values[1])) { @@ -145,7 +145,7 @@ public function where_between($Attribute, $Values) { * * @param string $Expr query expression * @param string $Field field to match $Expr against. Default is *, which means all available fields - * @return current Sphinxql query object + * @return current SphinxqlQuery object */ public function where_match($Expr, $Field = '*', $Escape = true) { if (empty($Expr)) { @@ -168,7 +168,7 @@ public function where_match($Expr, $Field = '*', $Escape = true) { * @param string $Attribute attribute to use for sorting. * Passing an empty attribute value will clear the current sort settings * @param string $Mode sort method to apply to the selected attribute - * @return current Sphinxql query object + * @return current SphinxqlQuery object */ public function order_by($Attribute = false, $Mode = false) { if (empty($Attribute)) { @@ -184,7 +184,7 @@ public function order_by($Attribute = false, $Mode = false) { * * @param string $Attribute group matches with the same $Attribute value. * Passing an empty attribute value will clear the current group settings - * @return current Sphinxql query object + * @return current SphinxqlQuery object */ public function group_by($Attribute = false) { if (empty($Attribute)) { @@ -201,7 +201,7 @@ public function group_by($Attribute = false) { * @param string $Attribute attribute to use for sorting. * Passing an empty attribute will clear the current group sort settings * @param string $Mode sort method to apply to the selected attribute - * @return current Sphinxql query object + * @return current SphinxqlQuery object */ public function order_group_by($Attribute = false, $Mode = false) { if (empty($Attribute)) { @@ -218,7 +218,7 @@ public function order_group_by($Attribute = false, $Mode = false) { * @param int $Offset number of matches to discard * @param int $Limit number of matches to return * @param int $MaxMatches number of results to store in the Sphinx server's memory. Must be >= ($Offset+$Limit) - * @return current Sphinxql query object + * @return current SphinxqlQuery object */ public function limit($Offset, $Limit, $MaxMatches = SPHINX_MAX_MATCHES) { $this->Limits = "$Offset, $Limit"; @@ -231,7 +231,7 @@ public function limit($Offset, $Limit, $MaxMatches = SPHINX_MAX_MATCHES) { * * @param string $Name setting name * @param mixed $Value value - * @return current Sphinxql query object + * @return current SphinxqlQuery object */ public function set($Name, $Value) { $this->Options[$Name] = $Value; @@ -265,6 +265,7 @@ private function build_query() { } if (!empty($this->Filters)) { $this->QueryString .= "\nWHERE ".implode("\n\tAND ", $this->Filters); + unset($this->Filters['expr']); } if (!empty($this->GroupBy)) { $this->QueryString .= "\nGROUP BY $this->GroupBy"; @@ -288,7 +289,7 @@ private function build_query() { * Construct and send the query. Register the query in the global Sphinxql object * * @param bool GetMeta whether to fetch meta data for the executed query. Default is yes - * @return Sphinxql result object + * @return SphinxqlResult object */ public function query($GetMeta = true) { $QueryStartTime = microtime(true); @@ -310,7 +311,7 @@ public function query($GetMeta = true) { * * @param string Query query expression * @param bool GetMeta whether to fetch meta data for the executed query. Default is yes - * @return Sphinxql result object + * @return SphinxqlResult object */ public function raw_query($Query, $GetMeta = true) { $this->QueryString = $Query; @@ -321,7 +322,7 @@ public function raw_query($Query, $GetMeta = true) { * Run a pre-processed query. Only used internally * * @param bool GetMeta whether to fetch meta data for the executed query - * @return Sphinxql result object + * @return SphinxqlResult object */ private function send_query($GetMeta) { if (!$this->QueryString) { @@ -368,6 +369,16 @@ private function get_meta() { return $this->raw_query("SHOW META", false)->to_pair(0, 1); } + /** + * Copy attribute filters from another SphinxqlQuery object + * + * @param SphinxqlQuery $SphQLSource object to copy the filters from + * @return current SphinxqlQuery object + */ + public function copy_attributes_from($SphQLSource) { + $this->Filters = $SphQLSource->Filters; + } + /** * Store error messages */ diff --git a/classes/tags.class.php b/classes/tags.class.php index 8ab798c1..efa8b89b 100644 --- a/classes/tags.class.php +++ b/classes/tags.class.php @@ -197,30 +197,34 @@ public static function get_aliases() { public static function remove_aliases($Tags) { $TagAliases = self::get_aliases(); - $End = count($Tags['include']); - for ($i = 0; $i < $End; $i++) { - foreach ($TagAliases as $TagAlias) { - if ($Tags['include'][$i] === $TagAlias['BadTag']) { - $Tags['include'][$i] = $TagAlias['AliasTag']; - break; + if (isset($Tags['include'])) { + $End = count($Tags['include']); + for ($i = 0; $i < $End; $i++) { + foreach ($TagAliases as $TagAlias) { + if ($Tags['include'][$i] === $TagAlias['BadTag']) { + $Tags['include'][$i] = $TagAlias['AliasTag']; + break; + } } } + // Only keep unique entries after unifying tag standard + $Tags['include'] = array_unique($Tags['include']); } - $End = count($Tags['exclude']); - for ($i = 0; $i < $End; $i++) { - foreach ($TagAliases as $TagAlias) { - if (substr($Tags['exclude'][$i], 1) === $TagAlias['BadTag']) { - $Tags['exclude'][$i] = '!'.$TagAlias['AliasTag']; - break; + if (isset($Tags['exclude'])) { + $End = count($Tags['exclude']); + for ($i = 0; $i < $End; $i++) { + foreach ($TagAliases as $TagAlias) { + if (substr($Tags['exclude'][$i], 1) === $TagAlias['BadTag']) { + $Tags['exclude'][$i] = '!'.$TagAlias['AliasTag']; + break; + } } } + // Only keep unique entries after unifying tag standard + $Tags['exclude'] = array_unique($Tags['exclude']); } - // Only keep unique entries after unifying tag standard - $Tags['include'] = array_unique($Tags['include']); - $Tags['exclude'] = array_unique($Tags['exclude']); - return $Tags; } diff --git a/classes/tools.class.php b/classes/tools.class.php index 435dd3a1..8505ba76 100644 --- a/classes/tools.class.php +++ b/classes/tools.class.php @@ -21,7 +21,6 @@ public static function site_ban_ip($IP) { G::$DB->set_query_id($QueryID); G::$Cache->cache_value('ip_bans_'.$A, $IPBans, 0); } - $Debug->log_var($IPBans, 'IP bans for class '.$A); foreach ($IPBans as $Index => $IPBan) { list ($ID, $FromIP, $ToIP) = $IPBan; if ($IPNum >= $FromIP && $IPNum <= $ToIP) { @@ -104,9 +103,21 @@ public static function get_host_by_ip($IP) { * @return a span with JavaScript code */ public static function get_host_by_ajax($IP) { - static $ID = 0; - ++$ID; - return 'Resolving host...'; + static $IPs = array(); + $Class = strtr($IP, '.', '-'); + $HTML = 'Resolving host...'; + if (!isset($IPs[$IP])) { + $HTML .= ''; + } + $HTML .= ''; + $IPs[$IP] = 1; + return $HTML; } @@ -146,12 +157,23 @@ public static function display_ip($IP) { } public static function get_country_code_by_ajax($IP) { - static $ID = 0; - ++$ID; - return 'Resolving CC...'; + static $IPs = array(); + $Class = strtr($IP, '.', '-'); + $HTML = 'Resolving CC...'; + if (!isset($IPs[$IP])) { + $HTML .= ''; + } + $HTML .= ''; + $IPs[$IP] = 1; + return $HTML; } - /** * Disable an array of users. * diff --git a/classes/top10view.class.php b/classes/top10view.class.php index c05f36b0..681e7bce 100644 --- a/classes/top10view.class.php +++ b/classes/top10view.class.php @@ -11,6 +11,7 @@ public static function render_linkbox($Selected) { + attribute name for ungrouped torrent page + */ + public static $SortOrders = array( + 'year' => 'year', + 'time' => 'id', + 'size' => 'size', + 'seeders' => 'seeders', + 'leechers' => 'leechers', + 'snatched' => 'snatched', + 'random' => 1); + + /** + * Map of sort mode => attribute name for grouped torrent page + */ + private static $SortOrdersGrouped = array( + 'year' => 'year', + 'time' => 'id', + 'size' => 'maxsize', + 'seeders' => 'sumseeders', + 'leechers' => 'sumleechers', + 'snatched' => 'sumsnatched', + 'random' => 1); + + /** + * Map of sort mode => aggregate expression required for some grouped sort orders + */ + private static $AggregateExp = array( + 'size' => 'MAX(size) AS maxsize', + 'seeders' => 'SUM(seeders) AS sumseeders', + 'leechers' => 'SUM(leechers) AS sumleechers', + 'snatched' => 'SUM(snatched) AS sumsnatched'); + + /** + * Map of attribute name => global variable name with list of values that can be used for filtering + */ + private static $Attributes = array( + 'filter_cat' => false, + 'releasetype' => 'ReleaseTypes', + 'freetorrent' => false, + 'hascue' => false, + 'haslog' => false, + 'scene' => false, + 'vanityhouse' => false, + 'year' => false); + + /** + * List of fields that can be used for fulltext searches + */ + private static $Fields = array( + 'artistname' => 1, + 'cataloguenumber' => 1, + 'description' => 1, + 'encoding' => 1, + 'filelist' => 1, + 'format' => 1, + 'groupname' => 1, + 'media' => 1, + 'recordlabel' => 1, + 'remastercataloguenumber' => 1, + 'remasterrecordlabel' => 1, + 'remastertitle' => 1, + 'remasteryear' => 1, + 'searchstr' => 1, + 'taglist' => 1); + + /** + * List of torrent-specific fields that can be used for filtering + */ + private static $TorrentFields = array( + 'description' => 1, + 'encoding' => 1, + 'filelist' => 1, + 'format' => 1, + 'media' => 1, + 'remastercataloguenumber' => 1, + 'remasterrecordlabel' => 1, + 'remastertitle' => 1, + 'remasteryear' => 1); + + /** + * Some form field names don't match the ones in the index + */ + private static $FormsToFields = array( + 'searchstr' => '(groupname,artistname,yearfulltext)'); + + /** + * Specify the operator type to use for fields. Empty key sets the default + */ + private static $FieldOperators = array( + '' => self::SPH_BOOL_AND, + 'encoding' => self::SPH_BOOL_OR, + 'format' => self::SPH_BOOL_OR, + 'media' => self::SPH_BOOL_OR); + + /** + * Specify the separator character to use for fields. Empty key sets the default + */ + private static $FieldSeparators = array( + '' => ' ', + 'encoding' => '|', + 'format' => '|', + 'media' => '|', + 'taglist' => ','); + + /** + * Primary SphinxqlQuery object used to get group IDs or torrent IDs for ungrouped searches + */ + private $SphQL; + + /** + * Second SphinxqlQuery object used to get torrent IDs if torrent-specific fulltext filters are used + */ + private $SphQLTor; + + /** + * Ordered result array or false if query resulted in an error + */ + private $SphResults; + + /** + * Requested page + */ + private $Page; + + /** + * Number of results per page + */ + private $PageSize; + + /** + * Number of results + */ + private $NumResults = 0; + + /** + * Array with info from all matching torrent groups + */ + private $Groups = array(); + + /** + * True if the NOT operator can be used. Sphinx needs at least one positive search condition + */ + private $EnableNegation = false; + + /** + * Whether any filters were used + */ + private $Filtered = false; + + /** + * Whether the random sort order is selected + */ + private $Random = false; + + /** + * Storage for fulltext search terms + * ['Field name' => [ + * 'include' => [], + * 'exclude' => [], + * 'operator' => self::SPH_BOOL_AND | self::SPH_BOOL_OR + * ]], ... + */ + private $Terms = array(); + + /** + * Unprocessed search terms for retrieval + */ + private $RawTerms = array(); + + /** + * Storage for used torrent-specific attribute filters + * ['Field name' => 'Search expression', ...] + */ + private $UsedTorrentAttrs = array(); + + /** + * Storage for used torrent-specific fulltext fields + * ['Field name' => 'Search expression', ...] + */ + private $UsedTorrentFields = array(); + + /** + * Initialize and configure a TorrentSearch object + * + * @param bool $GroupResults whether results should be grouped by group id + * @param string $OrderBy attribute to use for sorting the results + * @param string $OrderWay Whether to use ascending or descending order + * @param int $Page Page number to display + * @param int $PageSize Number of results per page + */ + public function __construct($GroupResults, $OrderBy, $OrderWay, $Page, $PageSize) { + if ($GroupResults && !isset(self::$SortOrdersGrouped[$OrderBy]) + || !$GroupResults && !isset(self::$SortOrders[$OrderBy]) + || !in_array($OrderWay, array('asc', 'desc')) + ) { + global $Debug; + $ErrMsg = "TorrentSearch constructor arguments:\n" . print_r(func_get_args(), true); + $Debug->analysis('Bad arguments in TorrentSearch constructor', $ErrMsg, 3600*24); + error('-1'); + } + if (!is_number($Page) || $Page < 1) { + $Page = 1; + } + if (check_perms('site_search_many')) { + $this->Page = $Page; + } else { + $this->Page = min($Page, SPHINX_MAX_MATCHES / $PageSize); + } + $ResultLimit = $PageSize; + $this->PageSize = $PageSize; + $this->GroupResults = $GroupResults; + $this->SphQL = new SphinxqlQuery(); + if ($OrderBy === 'random') { + $this->SphQL->select('id, groupid') + ->order_by('RAND()', ''); + $this->Random = true; + $this->Page = 1; + if ($GroupResults) { + // Get more results because ORDER BY RAND() can't be used in GROUP BY queries + $ResultLimit *= 5; + } + } elseif ($GroupResults) { + $Select = 'groupid'; + if (isset(self::$AggregateExp[$OrderBy])) { + $Select .= ', ' . self::$AggregateExp[$OrderBy]; + } + $this->SphQL->select($Select) + ->group_by('groupid') + ->order_group_by(self::$SortOrdersGrouped[$OrderBy], $OrderWay) + ->order_by(self::$SortOrdersGrouped[$OrderBy], $OrderWay); + } else { + $this->SphQL->select('id, groupid') + ->order_by(self::$SortOrders[$OrderBy], $OrderWay); + } + $Offset = ($this->Page - 1) * $ResultLimit; + $MaxMatches = $Offset + $ResultLimit; + $this->SphQL->from('torrents, delta') + ->limit($Offset, $ResultLimit, $MaxMatches); + } + + /** + * Process search terms and run the main query + * + * @param array $Terms Array containing all search terms (e.g. $_GET) + * @return array List of matching group IDs with torrent ID as key for ungrouped results + */ + public function query($Terms = array()) { + $this->process_search_terms($Terms); + $this->build_query(); + $this->run_query(); + $this->process_results(); + return $this->SphResults; + } + + /** + * Internal function that runs the queries needed to get the desired results + */ + private function run_query() { + $SphQLResult = $this->SphQL->query(); + if ($SphQLResult->Errno > 0) { + $this->SphResults = false; + return; + } + if ($this->Random && $this->GroupResults) { + $TotalCount = $SphQLResult->get_meta('total_found'); + $this->SphResults = $SphQLResult->collect('groupid'); + $GroupIDs = array_keys($this->SphResults); + $GroupCount = count($GroupIDs); + while ($SphQLResult->get_meta('total') < $TotalCount && $GroupCount < $this->PageSize) { + // Make sure we get $PageSize results, or all of them if there are less than $PageSize hits + $this->SphQL->where('groupid', $GroupIDs, true); + $SphQLResult = $this->SphQL->query(); + if (!$SphQLResult->has_results()) { + break; + } + $this->SphResults += $SphQLResult->collect('groupid'); + $GroupIDs = array_keys($this->SphResults); + $GroupCount = count($GroupIDs); + } + if ($GroupCount > $this->PageSize) { + $this->SphResults = array_slice($this->SphResults, 0, $this->PageSize, true); + } + $this->NumResults = count($this->SphResults); + } else { + $this->NumResults = (int)$SphQLResult->get_meta('total_found'); + if ($this->GroupResults) { + $this->SphResults = $SphQLResult->collect('groupid'); + } else { + $this->SphResults = $SphQLResult->to_pair('id', 'groupid'); + } + } + } + + /** + * Process search terms and store the parts in appropriate arrays until we know if + * the NOT operator can be used + */ + private function build_query() { + foreach ($this->Terms as $Field => $Words) { + $SearchString = ''; + if (isset(self::$FormsToFields[$Field])) { + $Field = self::$FormsToFields[$Field]; + } + $QueryParts = array('include' => array(), 'exclude' => array()); + if (!$this->EnableNegation && !empty($Words['exclude'])) { + $Words['include'] = $Words['exclude']; + unset($Words['exclude']); + } + if (!empty($Words['include'])) { + foreach ($Words['include'] as $Word) { + $QueryParts['include'][] = Sphinxql::sph_escape_string($Word); + } + } + if (!empty($Words['exclude'])) { + foreach ($Words['exclude'] as $Word) { + $QueryParts['exclude'][] = '!' . Sphinxql::sph_escape_string(substr($Word, 1)); + } + } + if (!empty($QueryParts)) { + if (isset($Words['operator'])) { + // Is the operator already specified? + $Operator = $Words['operator']; + } elseif(isset(self::$FieldOperators[$Field])) { + // Does this field have a non-standard operator? + $Operator = self::$FieldOperators[$Field]; + } else { + // Go for the default operator + $Operator = self::$FieldOperators['']; + } + if (!empty($QueryParts['include'])) { + $SearchString .= '( ' . implode($Operator, $QueryParts['include']) . ' ) '; + } + if (!empty($QueryParts['exclude'])) { + $SearchString .= implode(' ', $QueryParts['exclude']); + } + $this->SphQL->where_match($SearchString, $Field, false); + if (isset(self::$TorrentFields[$Field])) { + $this->UsedTorrentFields[$Field] = $SearchString; + } + $this->Filtered = true; + } + } + } + + /** + * Look at each search term and figure out what to do with it + * + * @param array $Terms Array with search terms from query() + */ + private function process_search_terms($Terms) { + foreach ($Terms as $Key => $Term) { + if (isset(self::$Fields[$Key])) { + $this->process_field($Key, $Term); + } elseif (isset(self::$Attributes[$Key])) { + $this->process_attribute($Key, $Term); + } + $this->RawTerms[$Key] = $Term; + } + $this->post_process_fields(); + } + + /** + * Process attribute filters and store them in case we need to post-process grouped results + * + * @param string $Attribute Name of the attribute to filter against + * @param mixed $Value The filter's condition for a match + */ + private function process_attribute($Attribute, $Value) { + if ($Value === '') { + return; + } + switch ($Attribute) { + case 'year': + if (!$this->search_year($Value)) { + return; + } + break; + + case 'haslog': + if ($Value == 0) { + $this->SphQL->where('haslog', 0); + } elseif ($Value == 100) { + $this->SphQL->where('logscore', 100); + } elseif ($Value < 0) { + $this->SphQL->where_lt('logscore', 100); + $this->SphQL->where('haslog', 1); + } else { + $this->SphQL->where('haslog', 1); + } + $this->UsedTorrentAttrs['haslog'] = $Value; + break; + + case 'freetorrent': + if ($Value == 3) { + $this->SphQL->where('freetorrent', 0, true); + $this->UsedTorrentAttrs['freetorrent'] = 3; + } elseif ($Value >= 0 && $Value < 3) { + $this->SphQL->where('freetorrent', $Value); + $this->UsedTorrentAttrs[$Attribute] = $Value; + } else { + return; + } + break; + + case 'filter_cat': + if (!is_array($Value)) { + $Value = array_fill_keys(explode('|', $Value), 1); + } + $CategoryFilter = array(); + foreach (array_keys($Value) as $Category) { + if (is_number($Category)) { + $CategoryFilter[] = $Category; + } else { + global $Categories; + $ValidValues = array_map('strtolower', $Categories); + if (($CategoryID = array_search(strtolower($Category), $ValidValues)) !== false) { + $CategoryFilter[] = $CategoryID + 1; + } + } + } + if (empty($CategoryFilter)) { + $CategoryFilter = 0; + } + $this->SphQL->where('categoryid', $CategoryFilter); + break; + + default: + if (!is_number($Value) && self::$Attributes[$Attribute] !== false) { + // Check if the submitted value can be converted to a valid one + $ValidValuesVarname = self::$Attributes[$Attribute]; + global $$ValidValuesVarname; + $ValidValues = array_map('strtolower', $$ValidValuesVarname); + if (($Value = array_search(strtolower($Value), $ValidValues)) === false) { + // Force the query to return 0 results if value is still invalid + $Value = max(array_keys($ValidValues)) + 1; + } + } + $this->SphQL->where($Attribute, $Value); + $this->UsedTorrentAttrs[$Attribute] = $Value; + break; + } + $this->Filtered = true; + } + + /** + * Look at a fulltext search term and figure out if it needs special treatment + * + * @param string $Field Name of the search field + * @param string $Term Search expression for the field + */ + private function process_field($Field, $Term) { + $Term = trim($Term); + if ($Term === '') { + return; + } + if ($Field === 'searchstr') { + $this->search_basic($Term); + } elseif ($Field === 'filelist') { + $this->search_filelist($Term); + } elseif ($Field === 'taglist') { + $this->search_taglist($Term); + } else { + $this->add_field($Field, $Term); + } + } + + /** + * Some fields may require post-processing + */ + private function post_process_fields() { + if (isset($this->Terms['taglist'])) { + // Replace bad tags with tag aliases + $this->Terms['taglist'] = Tags::remove_aliases($this->Terms['taglist']); + if (isset($this->RawTerms['tags_type']) && (int)$this->RawTerms['tags_type'] === self::TAGS_ANY) { + $this->Terms['taglist']['operator'] = self::SPH_BOOL_OR; + } + // Update the RawTerms array so get_terms() can return the corrected search terms + if (isset($this->Terms['taglist']['include'])) { + $AllTags = $this->Terms['taglist']['include']; + } else { + $AllTags = array(); + } + if (isset($this->Terms['taglist']['exclude'])) { + $AllTags = array_merge($AllTags, $this->Terms['taglist']['exclude']); + } + $this->RawTerms['taglist'] = str_replace('_', '.', implode(', ', $AllTags)); + } + } + + /** + * Handle magic keywords in the basic torrent search + * + * @param string $Term Given search expression + */ + private function search_basic($Term) { + global $Bitrates, $Formats, $Media; + $SearchBitrates = array_map('strtolower', $Bitrates); + array_push($SearchBitrates, 'v0', 'v1', 'v2', '24bit'); + $SearchFormats = array_map('strtolower', $Formats); + $SearchMedia = array_map('strtolower', $Media); + + foreach (explode(' ', $Term) as $Word) { + if (in_array($Word, $SearchBitrates)) { + $this->add_word('encoding', $Word); + } elseif (in_array($Word, $SearchFormats)) { + $this->add_word('format', $Word); + } elseif (in_array($Word, $SearchMedia)) { + $this->add_word('media', $Word); + } elseif ($Word === '100%') { + $this->process_attribute('haslog', 100); + } elseif ($Word === '!100%') { + $this->process_attribute('haslog', -1); + } else { + $this->add_word('searchstr', $Word); + } + } + } + + /** + * Use phrase boundary for file searches to make sure we don't count + * partial hits from multiple files + * + * @param string $Term Given search expression + */ + private function search_filelist($Term) { + $SearchString = '"' . Sphinxql::sph_escape_string($Term) . '"~20'; + $this->SphQL->where_match($SearchString, 'filelist', false); + $this->UsedTorrentFields['filelist'] = $SearchString; + $this->EnableNegation = true; + $this->Filtered = true; + } + + /** + * Prepare tag searches before sending them to the normal treatment + * + * @param string $Term Given search expression + */ + private function search_taglist($Term) { + $Term = strtr($Term, '.', '_'); + $this->add_field('taglist', $Term); + } + + /** + * The year filter accepts a range. Figure out how to handle the filter value + * + * @param string $Term Filter condition. Can be an integer or a range with the format X-Y + * @return bool True if parameters are valid + */ + private function search_year($Term) { + $Years = explode('-', $Term); + if (count($Years) === 1 && is_number($Years[0])) { + // Exact year + $this->SphQL->where('year', $Years[0]); + } elseif (count($Years) === 2) { + if (empty($Years[0]) && is_number($Years[1])) { + // Range: 0 - 2005 + $this->SphQL->where_lt('year', $Years[1], true); + } elseif (empty($Years[1]) && is_number($Years[0])) { + // Range: 2005 - 2^32-1 + $this->SphQL->where_gt('year', $Years[0], true); + } elseif (is_number($Years[0]) && is_number($Years[1])) { + // Range: 2005 - 2009 + $this->SphQL->where_between('year', array(min($Years), max($Years))); + } else { + // Invalid input + return false; + } + } else { + // Invalid input + return false; + } + return true; + } + + /** + * Add a field filter that doesn't need special treatment + * + * @param string $Field Name of the search field + * @param string $Term Search expression for the field + */ + private function add_field($Field, $Term) { + if (isset(self::$FieldSeparators[$Field])) { + $Separator = self::$FieldSeparators[$Field]; + } else { + $Separator = self::$FieldSeparators['']; + } + $Words = explode($Separator, $Term); + foreach ($Words as $Word) { + $this->add_word($Field, $Word); + } + } + + /** + * Add a keyword to the array of search terms + * + * @param string $Field Name of the search field + * @param string $Word Keyword + */ + private function add_word($Field, $Word) { + $Word = trim($Word); + // Skip isolated hyphens to enable "Artist - Title" searches + if ($Word === '' || $Word === '-') { + return; + } + if ($Word[0] === '!' && strlen($Word) >= 2 && strpos($Word, '!', 1) === false) { + $this->Terms[$Field]['exclude'][] = $Word; + } else { + $this->Terms[$Field]['include'][] = $Word; + $this->EnableNegation = true; + } + } + + /** + * @return array Torrent group information for the matches from Torrents::get_groups + */ + public function get_groups() { + return $this->Groups; + } + + /** + * @param string $Type Field or attribute name + * @return string Unprocessed search terms + */ + public function get_terms($Type) { + return isset($this->RawTerms[$Type]) ? $this->RawTerms[$Type] : ''; + } + + /** + * @return int Result count + */ + public function record_count() { + return $this->NumResults; + } + + /** + * @return bool Whether any filters were used + */ + public function has_filters() { + return $this->Filtered; + } + + /** + * @return bool Whether any torrent-specific fulltext filters were used + */ + public function need_torrent_ft() { + return $this->GroupResults && $this->NumResults > 0 && !empty($this->UsedTorrentFields); + } + + /** + * Get torrent group info and remove any torrents that don't match + */ + private function process_results() { + if (count($this->SphResults) == 0) { + return; + } + $this->Groups = Torrents::get_groups($this->SphResults); + if ($this->need_torrent_ft()) { + // Query Sphinx for torrent IDs if torrent-specific fulltext filters were used + $this->filter_torrents_sph(); + } elseif ($this->GroupResults) { + // Otherwise, let PHP discard unmatching torrents + $this->filter_torrents_internal(); + } + // Ungrouped searches don't need any additional filtering + } + + /** + * Build and run a query that gets torrent IDs from Sphinx when fulltext filters + * were used to get primary results and they are grouped + */ + private function filter_torrents_sph() { + $AllTorrents = array(); + foreach ($this->Groups as $GroupID => $Group) { + if (!empty($Group['Torrents'])) { + $AllTorrents += array_fill_keys(array_keys($Group['Torrents']), $GroupID); + } + } + $TorrentCount = count($AllTorrents); + $this->SphQLTor = new SphinxqlQuery(); + $this->SphQLTor->select('id')->from('torrents, delta'); + foreach ($this->UsedTorrentFields as $Field => $Term) { + $this->SphQLTor->where_match($Term, $Field, false); + } + $this->SphQLTor->copy_attributes_from($this->SphQL); + $this->SphQLTor->where('id', array_keys($AllTorrents))->limit(0, $TorrentCount, $TorrentCount); + $SphQLResultTor = $this->SphQLTor->query(); + $MatchingTorrentIDs = $SphQLResultTor->to_pair('id', 'id'); + foreach ($AllTorrents as $TorrentID => $GroupID) { + if (!isset($MatchingTorrentIDs[$TorrentID])) { + unset($this->Groups[$GroupID]['Torrents'][$TorrentID]); + } + } + } + + /** + * Non-Sphinx method of collecting IDs of torrents that match any + * torrent-specific attribute filters that were used in the search query + */ + private function filter_torrents_internal() { + foreach ($this->Groups as $GroupID => $Group) { + if (empty($Group['Torrents'])) { + continue; + } + foreach ($Group['Torrents'] as $TorrentID => $Torrent) { + if (!$this->filter_torrent_internal($Torrent)) { + unset($this->Groups[$GroupID]['Torrents'][$TorrentID]); + } + } + } + } + + /** + * Post-processing to determine if a torrent is a real hit or if it was + * returned because another torrent in the group matched. Only used if + * there are no torrent-specific fulltext conditions + * + * @param array $Torrent Torrent array, probably from Torrents::get_groups() + * @return bool True if it's a real hit + */ + private function filter_torrent_internal($Torrent) { + if (isset($this->UsedTorrentAttrs['freetorrent'])) { + $FilterValue = $this->UsedTorrentAttrs['freetorrent']; + if ($FilterValue == '3' && $Torrent['FreeTorrent'] == '0') { + // Either FL or NL is ok + return false; + } elseif ($FilterValue != '3' && $FilterValue != (int)$Torrent['FreeTorrent']) { + return false; + } + } + if (isset($this->UsedTorrentAttrs['hascue'])) { + if ($this->UsedTorrentAttrs['hascue'] != (int)$Torrent['HasCue']) { + return false; + } + } + if (isset($this->UsedTorrentAttrs['haslog'])) { + $FilterValue = $this->UsedTorrentAttrs['haslog']; + if ($FilterValue == '0') { + // No logs + $Pass = empty($Torrent['HasLog']); + } elseif ($FilterValue == '100') { + // 100% logs + $Pass = $Torrent['LogScore'] == '100'; + } elseif ($FilterValue < 0) { + // Unscored or <100% logs + $Pass = !empty($Torrent['HasLog']) && $Torrent['LogScore'] != '100'; + } else { + // Any log score + $Pass = $Torrent['HasLog'] == '1'; + } + if (!$Pass) { + return false; + } + } + if (isset($this->UsedTorrentAttrs['scene'])) { + if ($this->UsedTorrentAttrs['scene'] != (int)$Torrent['Scene']) { + return false; + } + } + return true; + } +} diff --git a/classes/tracker.class.php b/classes/tracker.class.php index e6c940b8..cb7f4383 100644 --- a/classes/tracker.class.php +++ b/classes/tracker.class.php @@ -130,6 +130,8 @@ private static function send_request($Get, $MaxAttempts = 1, &$Err = false) { if ($Sleep) { sleep($Sleep); } + $Sleep = 6; + // Send request $File = fsockopen(TRACKER_HOST, TRACKER_PORT, $ErrorNum, $ErrorString); if ($File) { @@ -140,7 +142,6 @@ private static function send_request($Get, $MaxAttempts = 1, &$Err = false) { } } else { $Err = "Failed to fsockopen() - $ErrorNum - $ErrorString"; - $Sleep = 6; continue; } diff --git a/docs/CHANGES.txt b/docs/CHANGES.txt index da27b69d..b02b6c5d 100644 --- a/docs/CHANGES.txt +++ b/docs/CHANGES.txt @@ -1,5 +1,76 @@ CHANGE LOG +2014-11-06 by porkpie + Create a TorrentSearch class that does most of the torrent search job. + Add PHP post-processing of matches if a search query doesn't need + any torrent-specific fulltext conditions. + Comments + +2014-11-03 by downinthings +Fix comments on User API + +2014-10-30 by tobbez +Add stats API method + +2014-10-27 by porkpie +Clear news cache immediately after updating the database + +2014-10-20 by Narcolepsy +Consolidated commit list since 2014-04-20: + +Fix some LTR issues (bug thread 189685) +Move loading comments before View::show_header. This should fix subscriptions and quote notifications appearing when a user is already on the page of that last subscription +Replace some hardcoded IRC channels with configuration parameters +Fix HTML in for non-{Music, Audiobooks, Comedy} requests (bug thread 189855) +Fix: link directly to the torrent on bad files/folders better.php pages (thread 190009) +Update internal cache when a cache key is changed +This should make quote notifications disappear when viewing threads (#183300) +Cast user id to int on user profile pages to get rid of trailing period +Remove WhatMan's spellcheck tool (seriously it was crap) +Fix a few cache key typos: + Blog => blog + requests_$RequestID => request_$RequestID + users_stats_$UserID => user_stats_$UserID +Fix some sorting issues related to Torrents::get_groups not returning the groups in the same order they were passed in; DO NOT iterate over Torrents::get_groups results... +Fix image in bookmarks JSON API +Fix comments appearing on the wrong page (bug thread 190203) +Passwords aren't limited to 40 characters +Fix better.php/single and rename some variables so they make some sense + Bug thread #190289 +Don't continue to process forum post deletes if the post or thread doesn't exist +Trusting rdns is bad +Fix two cache issues in the schedule + - Permissions of users that are demoted to User/Member for low ratio or upload were not updated/cleared, which meant that e.g. demoted PU users could still see the PU forum + - The cache was cleared before the promotion/demotion queries. This could cause a race condition if the cache key were filled again (through a Users::user_info call) +Fix some cache issues (bug thread 190802) +Speed up BBCode +BBCode profiling +Fix a bunch of bookmarks stuff. HTML/CSS consistency, jQuerified functions +Bye sizzle +show rank 4 instead of 5 when 5 points in pms +More accurate donor rank expiration times +Apparently users_donor_ranks can contain NULL values. + Change these to 0 after fetching to fix cache +Fix a grammar error and use SITE_NAME constant in the post-invite confirmation PM that gets sent to the inviter +Remove trailing whitespace in public Gazelle, improve grammar, add some missing spaces in 80char's CSS file +Remove extraneous whitespace from various files +Coding standards cleanup in script_start.php + Remove some WCD-specific smileys from public Gazelle + + Remove these: + * ackbar-what.png + * jesusfish-what.png + * louisdefunes-what.png +Add Tools::check_cidr_range to check if an IP is inside a given range in CIDR format +Torrent size is a 64-bit Sphinx attribute, so let's make use of it + Should partially fix bug #185554 +Fix SQLI in PushServer::push_pushbullet +Minor coding standards fixes in classes/pushserver.class.php +Fix User Upload Count API +fix lack of artist name in concert titles +add mailgun as alternative to self-hosting email, config changes in template to match +user api additional stats, patch from user downinthings + 2014-04-20 by SevenNationArmy Replace old IRC applet with Mibbit diff --git a/gazelle.sql b/gazelle.sql index 1ad91ba3..16ff96a4 100644 --- a/gazelle.sql +++ b/gazelle.sql @@ -1128,7 +1128,7 @@ CREATE TABLE `torrents` ( `Seeders` int(6) NOT NULL DEFAULT '0', `last_action` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `FreeTorrent` enum('0','1','2') NOT NULL DEFAULT '0', - `FreeLeechType` enum('0','1','2','3') NOT NULL DEFAULT '0', + `FreeLeechType` enum('0','1','2','3','4','5','6','7') NOT NULL DEFAULT '0', `Time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `Description` text, `Snatched` int(10) unsigned NOT NULL DEFAULT '0', diff --git a/ocelot-1.0.tar.gz b/ocelot-1.0.tar.gz new file mode 100644 index 00000000..324c72d0 Binary files /dev/null and b/ocelot-1.0.tar.gz differ diff --git a/sections/ajax/announcements.php b/sections/ajax/announcements.php index eff908b3..586b1097 100644 --- a/sections/ajax/announcements.php +++ b/sections/ajax/announcements.php @@ -77,7 +77,7 @@ } } -json_die("success", array( +json_print("success", array( 'announcements' => $JsonAnnouncements, 'blogPosts' => $JsonBlog )); diff --git a/sections/ajax/artist.php b/sections/ajax/artist.php index 77bb5087..e882abb8 100644 --- a/sections/ajax/artist.php +++ b/sections/ajax/artist.php @@ -342,7 +342,7 @@ function compare($X, $Y) { $Cache->cache_value($Key, $Data, 3600); -json_die("success", array( +json_print("success", array( 'id' => (int)$ArtistID, 'name' => $Name, 'notificationsEnabled' => $notificationsEnabled, diff --git a/sections/ajax/browse.php b/sections/ajax/browse.php index d8e7df7f..38d963f6 100644 --- a/sections/ajax/browse.php +++ b/sections/ajax/browse.php @@ -1,494 +1,44 @@ <? include(SERVER_ROOT.'/sections/torrents/functions.php'); -/** Start default parameters and validation **/ -// Setting default search options -if (!empty($_GET['setdefault'])) { - $UnsetList = array('page', 'setdefault'); - $UnsetRegexp = '/(&|^)('.implode('|', $UnsetList).')=.*?(&|$)/i'; - - $DB->query(" - SELECT SiteOptions - FROM users_info - WHERE UserID = '".db_string($LoggedUser['ID'])."'"); - list($SiteOptions) = $DB->next_record(MYSQLI_NUM, false); - if (!empty($SiteOptions)) { - $SiteOptions = unserialize($SiteOptions); - } else { - $SiteOptions = array(); - } - $SiteOptions['DefaultSearch'] = preg_replace($UnsetRegexp, '', $_SERVER['QUERY_STRING']); - $DB->query(" - UPDATE users_info - SET SiteOptions = '".db_string(serialize($SiteOptions))."' - WHERE UserID = '".db_string($LoggedUser['ID'])."'"); - $Cache->begin_transaction("user_info_heavy_$UserID"); - $Cache->update_row(false, array('DefaultSearch' => $SiteOptions['DefaultSearch'])); - $Cache->commit_transaction(0); - -// Clearing default search options -} elseif (!empty($_GET['cleardefault'])) { - $DB->query(" - SELECT SiteOptions - FROM users_info - WHERE UserID = '".db_string($LoggedUser['ID'])."'"); - list($SiteOptions) = $DB->next_record(MYSQLI_NUM, false); - $SiteOptions = unserialize($SiteOptions); - $SiteOptions['DefaultSearch'] = ''; - $DB->query(" - UPDATE users_info - SET SiteOptions = '".db_string(serialize($SiteOptions))."' - WHERE UserID = '".db_string($LoggedUser['ID'])."'"); - $Cache->begin_transaction("user_info_heavy_$UserID"); - $Cache->update_row(false, array('DefaultSearch' => '')); - $Cache->commit_transaction(0); - -// Use default search options -} elseif (empty($_SERVER['QUERY_STRING']) || (count($_GET) === 1 && isset($_GET['page']))) { - if (!empty($LoggedUser['DefaultSearch'])) { - if (!empty($_GET['page'])) { - $Page = $_GET['page']; - parse_str($LoggedUser['DefaultSearch'], $_GET); - $_GET['page'] = $Page; - } else { - parse_str($LoggedUser['DefaultSearch'], $_GET); - } - } -} -// Terms were not submitted via the search form -if (!isset($_GET['searchsubmit'])) { - $_GET['group_results'] = !$LoggedUser['DisableGrouping2']; -} - -if (isset($_GET['group_results']) && $_GET['group_results']) { - $_GET['group_results'] = 1; - $GroupResults = true; - $SortOrders = array( - // 'url attr' => [global order, order within group] - 'year' => array('year', 'year'), - 'time' => array('id', 'id'), - 'size' => array('maxsize', 'size'), - 'seeders' => array('sumseeders', 'seeders'), - 'leechers' => array('sumleechers', 'leechers'), - 'snatched' => array('sumsnatched', 'snatched'), - 'random' => false); - - $AggregateExp = array( - 'maxsize' => 'MAX(size) AS maxsize', - 'sumseeders' => 'SUM(seeders) AS sumseeders', - 'sumleechers' => 'SUM(leechers) AS sumleechers', - 'sumsnatched' => 'SUM(snatched) AS sumsnatched'); -} else { - $GroupResults = false; - $SortOrders = array( - 'year' => 'year', - 'time' => 'id', - 'size' => 'size', - 'seeders' => 'seeders', - 'leechers' => 'leechers', - 'snatched' => 'snatched', - 'random' => false); -} - -if (empty($_GET['order_by']) || !isset($SortOrders[$_GET['order_by']])) { - $_GET['order_by'] = 'time'; - $OrderBy = 'time'; // For header links -} else { - $OrderBy = $_GET['order_by']; -} - if (!empty($_GET['order_way']) && $_GET['order_way'] == 'asc') { $OrderWay = 'asc'; } else { - $_GET['order_way'] = 'desc'; $OrderWay = 'desc'; } -/** End default parameters and validation **/ - -/** Start preparation of property arrays **/ -array_pop($Bitrates); // remove 'other' -$SearchBitrates = array_merge($Bitrates, array('v0', 'v1', 'v2', '24bit')); - -foreach ($SearchBitrates as $ID => $Val) { - $SearchBitrates[$ID] = strtolower($Val); -} -foreach ($Formats as $ID => $Val) { - $SearchFormats[$ID] = strtolower($Val); -} -/** End preparation of property arrays **/ - -/** Start query preparation **/ -$SphQL = new SphinxqlQuery(); -$SphQLTor = new SphinxqlQuery(); - -if ($OrderBy == 'random') { - $SphQL->select('id, groupid, categoryid') - ->order_by('RAND()', ''); - $Random = true; -} elseif ($GroupResults) { - $OrderProperties = $SortOrders[$OrderBy]; - $SphQL->select('groupid, categoryid' . (isset($AggregateExp[$OrderProperties[0]]) ? ', '.$AggregateExp[$OrderProperties[0]] : '')) - ->group_by('groupid') - ->order_by($OrderProperties[0], $OrderWay) - ->order_group_by($OrderProperties[1], $OrderWay); - +if (empty($_GET['order_by']) || !isset(TorrentSearch::$SortOrders[$_GET['order_by']])) { + $OrderBy = 'time'; } else { - $SphQL->select('id, groupid, categoryid') - ->order_by($SortOrders[$OrderBy], $OrderWay); -} -$SphQL->from('torrents, delta'); -$SphQLTor->select('id, groupid')->from('torrents, delta'); -/** End query preparation **/ - -/** Start building search query **/ -$Filtered = false; -$EnableNegation = false; // Sphinx needs at least one positive search condition to support the NOT operator - -// File list searches make use of the proximity operator to ensure that all keywords match the same file -if (!empty($_GET['filelist'])) { - $SearchString = trim($_GET['filelist']); - if ($SearchString !== '') { - $SearchString = '"'.Sphinxql::sph_escape_string($_GET['filelist']).'"~20'; - $SphQL->where_match($SearchString, 'filelist', false); - $SphQLTor->where_match($SearchString, 'filelist', false); - $EnableNegation = true; - } + $OrderBy = $_GET['order_by']; } -// Collect all entered search terms to find out whether to enable the NOT operator -$SearchWords = array(); -foreach (array('artistname', 'groupname', 'recordlabel', 'cataloguenumber', - 'taglist', 'remastertitle', 'remasteryear', 'remasterrecordlabel', - 'remastercataloguenumber', 'encoding', 'format', 'media', 'description') as $Search) { - if (!empty($_GET[$Search])) { - $SearchString = trim($_GET[$Search]); - if ($SearchString !== '') { - $SearchWords[$Search] = array('include' => array(), 'exclude' => array()); - if ($Search == 'taglist') { - $SearchString = strtr($SearchString, '.', '_'); - $Words = explode(',', $SearchString); - } else { - $Words = explode(' ', $SearchString); - } - foreach ($Words as $Word) { - $Word = trim($Word); - // Skip isolated hyphens to enable "Artist - Title" searches - if ($Word === '-') { - continue; - } - if ($Word[0] === '!' && strlen($Word) >= 2) { - if (strpos($Word, '!', 1) === false) { - $SearchWords[$Search]['exclude'][] = $Word; - } else { - $SearchWords[$Search]['include'][] = $Word; - $EnableNegation = true; - } - } elseif ($Word !== '') { - $SearchWords[$Search]['include'][] = $Word; - $EnableNegation = true; - } - } - } - } +$GroupResults = !isset($_GET['group_results']) || $_GET['group_results'] != '0'; +$Page = !empty($_GET['page']) ? (int)$_GET['page'] : 1; +$Search = new TorrentSearch($GroupResults, $OrderBy, $OrderWay, $Page, TORRENTS_PER_PAGE); +$Results = $Search->query($_GET); +$Groups = $Search->get_groups(); +$NumResults = $Search->record_count(); + +if ($Results === false) { + json_die('error', 'Search returned an error. Make sure all parameters are valid and of the expected types.'); } - -//Simple search -if (!empty($_GET['searchstr'])) { - $SearchString = trim($_GET['searchstr']); - $Words = explode(' ', strtolower($SearchString)); - if (!empty($Words)) { - $FilterBitrates = $FilterFormats = array(); - $BasicSearch = array('include' => array(), 'exclude' => array()); - foreach ($Words as $Word) { - $Word = trim($Word); - // Skip isolated hyphens to enable "Artist - Title" searches - if ($Word === '-') { - continue; - } - if ($Word[0] === '!' && strlen($Word) >= 2) { - if ($Word === '!100%') { - $_GET['haslog'] = '-1'; - } elseif (strpos($Word, '!', 1) === false) { - $BasicSearch['exclude'][] = $Word; - } else { - $BasicSearch['include'][] = $Word; - $EnableNegation = true; - } - } elseif (in_array($Word, $SearchBitrates)) { - $FilterBitrates[] = $Word; - $EnableNegation = true; - } elseif (in_array($Word, $SearchFormats)) { - $FilterFormats[] = $Word; - $EnableNegation = true; - } elseif ($Word === '100%') { - $_GET['haslog'] = '100'; - } elseif ($Word !== '') { - $BasicSearch['include'][] = $Word; - $EnableNegation = true; - } - } - if (!$EnableNegation && !empty($BasicSearch['exclude'])) { - $BasicSearch['include'] = array_merge($BasicSearch['include'], $BasicSearch['exclude']); - unset($BasicSearch['exclude']); - } - $QueryParts = array(); - foreach ($BasicSearch['include'] as $Word) { - $QueryParts[] = Sphinxql::sph_escape_string($Word); - } - if (!empty($BasicSearch['exclude'])) { - foreach ($BasicSearch['exclude'] as $Word) { - $QueryParts[] = '!'.Sphinxql::sph_escape_string(substr($Word, 1)); - } - } - if (!empty($FilterBitrates)) { - $SearchString = implode(' ', $FilterBitrates); - $SphQL->where_match($SearchString, 'encoding', false); - $SphQLTor->where_match($SearchString, 'encoding', false); - } - if (!empty($FilterFormats)) { - $SearchString = implode(' ', $FilterFormats); - $SphQL->where_match($SearchString, 'format', false); - $SphQLTor->where_match($SearchString, 'format', false); - } - if (!empty($QueryParts)) { - $SearchString = implode(' ', $QueryParts); - $SphQL->where_match($SearchString, '(groupname,artistname,yearfulltext)', false); - $SphQLTor->where_match($SearchString, '(groupname,artistname,yearfulltext)', false); - } - } -} - -// Tag list -if (!empty($SearchWords['taglist'])) { - $_GET['tags_type'] = (!isset($_GET['tags_type']) || $_GET['tags_type'] == 1) ? '1' : '0'; - $TagType = (int)$_GET['tags_type']; - - //Get tags - $Tags = $SearchWords['taglist']; - - $TagFilter = Tags::tag_filter_sph($Tags, $EnableNegation, $TagType); - if (!empty($TagFilter['predicate'])) { - $SphQL->where_match($TagFilter['predicate'], 'taglist', false); - $SphQLTor->where_match($TagFilter['predicate'], 'taglist', false); - } - unset($SearchWords['taglist']); -} -elseif (!isset($_GET['tags_type'])) { - $_GET['tags_type'] = '1'; -} - -foreach ($SearchWords as $Search => $Words) { - $QueryParts = array(); - if (!$EnableNegation && !empty($Words['exclude'])) { - $Words['include'] = array_merge($Words['include'], $Words['exclude']); - unset($Words['exclude']); - } - foreach ($Words['include'] as $Word) { - $QueryParts[] = Sphinxql::sph_escape_string($Word); - } - if (!empty($Words['exclude'])) { - foreach ($Words['exclude'] as $Word) { - $QueryParts[] = '!'.Sphinxql::sph_escape_string(substr($Word, 1)); - } - } - if (!empty($QueryParts)) { - $SearchString = implode(' ', $QueryParts); - $SphQL->where_match($SearchString, $Search, false); - $SphQLTor->where_match($SearchString, $Search, false); - } -} - -if (!empty($_GET['year'])) { - $Years = explode('-', $_GET['year']); - if (is_number($Years[0]) || (empty($Years[0]) && !empty($Years[1]) && is_number($Years[1]))) { - if (count($Years) === 1) { - $SphQL->where('year', (int)$Years[0]); - $SphQLTor->where('year', (int)$Years[0]); - } else { - if (empty($Years[1]) || !is_number($Years[1])) { - $Years[1] = PHP_INT_MAX; - } elseif ($Years[0] > $Years[1]) { - $Years = array_reverse($Years); - } - $SphQL->where_between('year', array((int)$Years[0], (int)$Years[1])); - $SphQLTor->where_between('year', array((int)$Years[0], (int)$Years[1])); - } - } -} - -if (isset($_GET['haslog']) && $_GET['haslog'] !== '') { - if ($_GET['haslog'] === '100') { - $SphQL->where('logscore', 100); - $SphQLTor->where('logscore', 100); - } elseif ($_GET['haslog'] < 0) { - // Exclude torrents with log score equal to 100 - $SphQL->where('logscore', 100, true); - $SphQL->where('haslog', 1); - $SphQLTor->where('logscore', 100, true); - $SphQLTor->where('haslog', 1); - } elseif ($_GET['haslog'] == 0) { - $SphQL->where('haslog', 0); - $SphQLTor->where('haslog', 0); - } else { - $SphQL->where('haslog', 1); - $SphQLTor->where('haslog', 1); - } -} - -foreach (array('hascue', 'scene', 'vanityhouse', 'releasetype') as $Search) { - if (isset($_GET[$Search]) && $_GET[$Search] !== '') { - $SphQL->where($Search, $_GET[$Search]); - // Release type is group specific - if ($Search != 'releasetype') { - $SphQLTor->where($Search, $_GET[$Search]); - } - } -} - -if (isset($_GET['freetorrent']) && $_GET['freetorrent'] !== '') { - switch ($_GET['freetorrent']) { - case 0: // Only normal freeleech - $SphQL->where('freetorrent', 0); - $SphQLTor->where('freetorrent', 0); - break; - case 1: // Only free leech - $SphQL->where('freetorrent', 1); - $SphQLTor->where('freetorrent', 1); - break; - case 2: // Only neutral leech - $SphQL->where('freetorrent', 2); - $SphQLTor->where('freetorrent', 2); - break; - case 3: // Free or neutral leech - $SphQL->where('freetorrent', 0, true); - $SphQLTor->where('freetorrent', 0, true); - break; - } -} - -if (!empty($_GET['filter_cat'])) { - $SphQL->where('categoryid', array_keys($_GET['filter_cat'])); -} -/** End building search query **/ - -/** Run search query and collect results **/ -if (isset($Random) && $GroupResults) { - // ORDER BY RAND() can't be used together with GROUP BY, so we need some special tactics - $Page = 1; - $SphQL->limit(0, 5 * TORRENTS_PER_PAGE, 5 * TORRENTS_PER_PAGE); - $SphQLResult = $SphQL->query(); - $TotalCount = $SphQLResult->get_meta('total_found'); - $Results = $SphQLResult->to_array('groupid'); - $GroupIDs = array_keys($Results); - $GroupCount = count($GroupIDs); - while ($SphQLResult->get_meta('total') < $TotalCount && $GroupCount < TORRENTS_PER_PAGE) { - // Make sure we get TORRENTS_PER_PAGE results, or all of them if there are less than TORRENTS_PER_PAGE hits - $SphQL->where('groupid', $GroupIDs, true); - $SphQLResult = $SphQL->query(); - if (!$SphQLResult->has_results()) { - break; - } - $Results += $SphQLResult->to_array('groupid'); - $GroupIDs = array_keys($Results); - $GroupCount = count($GroupIDs); - } - if ($GroupCount > TORRENTS_PER_PAGE) { - $Results = array_slice($Results, 0, TORRENTS_PER_PAGE, true); - } - $GroupIDs = array_keys($Results); - $NumResults = count($Results); -} else { - if (!empty($_GET['page']) && is_number($_GET['page']) && $_GET['page'] > 0) { - if (check_perms('site_search_many')) { - $Page = $_GET['page']; - } else { - $Page = min(SPHINX_MAX_MATCHES / TORRENTS_PER_PAGE, $_GET['page']); - } - $Offset = ($Page - 1) * TORRENTS_PER_PAGE; - $SphQL->limit($Offset, TORRENTS_PER_PAGE, $Offset + TORRENTS_PER_PAGE); - } else { - $Page = 1; - $SphQL->limit(0, TORRENTS_PER_PAGE, TORRENTS_PER_PAGE); - } - $SphQLResult = $SphQL->query(); - $NumResults = $SphQLResult->get_meta('total_found'); - if ($GroupResults) { - $Results = $SphQLResult->to_array('groupid'); - $GroupIDs = array_keys($Results); - } else { - $Results = $SphQLResult->to_array('id'); - $GroupIDs = $SphQLResult->collect('groupid'); - } -} - -if (!check_perms('site_search_many') && $NumResults > SPHINX_MAX_MATCHES) { - $NumResults = SPHINX_MAX_MATCHES; -} - -if ($NumResults) { - $Groups = Torrents::get_groups($GroupIDs); - - if (!empty($Groups) && $GroupResults) { - $TorrentIDs = array(); - foreach ($Groups as $Group) { - if (!empty($Group['Torrents'])) { - $TorrentIDs = array_merge($TorrentIDs, array_keys($Group['Torrents'])); - } - } - $TorrentCount = count($TorrentIDs); - if ($TorrentCount > 0) { - // Get a list of all torrent ids that match the search query - $SphQLTor->where('id', $TorrentIDs)->limit(0, $TorrentCount, $TorrentCount); - $SphQLResultTor = $SphQLTor->query(); - $TorrentIDs = $SphQLResultTor->to_pair('id', 'id'); // Because isset() is faster than in_array() - } - } -} -/** End run search query and collect results **/ - if ($NumResults == 0) { - -$DB->query(" - SELECT - tags.Name, - ((COUNT(tags.Name) - 2) * (SUM(tt.PositiveVotes) - SUM(tt.NegativeVotes))) / (tags.Uses * 0.8) AS Score - FROM xbt_snatched AS s - INNER JOIN torrents AS t ON t.ID = s.fid - INNER JOIN torrents_group AS g ON t.GroupID = g.ID - INNER JOIN torrents_tags AS tt ON tt.GroupID = g.ID - INNER JOIN tags ON tags.ID = tt.TagID - WHERE s.uid = '$LoggedUser[ID]' - AND tt.TagID != '13679' - AND tt.TagID != '4820' - AND tt.TagID != '2838' - AND g.CategoryID = '1' - AND tags.Uses > '10' - GROUP BY tt.TagID - ORDER BY Score DESC - LIMIT 8"); - $JsonYouMightLike = array(); - while (list($Tag) = $DB->next_record()) { - $JsonYouMightLike[] = $Tag; - } - - - json_die("success", array( + json_die('success', array( 'results' => array(), - 'youMightLike' => $JsonYouMightLike + 'youMightLike' => array() // This slow and broken feature has been removed )); } $Bookmarks = Bookmarks::all_bookmarks('torrent'); $JsonGroups = array(); -foreach ($Results as $Result) { - $GroupID = $Result['groupid']; +foreach ($Results as $Key => $GroupID) { $GroupInfo = $Groups[$GroupID]; if (empty($GroupInfo['Torrents'])) { continue; } - $CategoryID = $Result['categoryid']; + $CategoryID = $GroupInfo['CategoryID']; $GroupYear = $GroupInfo['Year']; $ExtendedArtists = $GroupInfo['ExtendedArtists']; $GroupCatalogueNumber = $GroupInfo['CatalogueNumber']; @@ -500,15 +50,14 @@ $GroupTime = $MaxSize = $TotalLeechers = $TotalSeeders = $TotalSnatched = 0; foreach ($Torrents as $T) { $GroupTime = max($GroupTime, strtotime($T['Time'])); - if ($T['Size'] > $MaxSize) { - $MaxSize = $T['Size']; - } + $MaxSize = max($MaxSize, $T['Size']); $TotalLeechers += $T['Leechers']; $TotalSeeders += $T['Seeders']; $TotalSnatched += $T['Snatched']; } } else { - $Torrents = array($Result['id'] => $GroupInfo['Torrents'][$Result['id']]); + $TorrentID = $Key; + $Torrents = array($TorrentID => $GroupInfo['Torrents'][$TorrentID]); } $TagList = explode(' ', str_replace('_', '.', $GroupInfo['TagList'])); @@ -516,22 +65,12 @@ if (!empty($ExtendedArtists[1]) || !empty($ExtendedArtists[4]) || !empty($ExtendedArtists[5]) || !empty($ExtendedArtists[6])) { unset($ExtendedArtists[2]); unset($ExtendedArtists[3]); - $DisplayName = Artists::display_artists($ExtendedArtists, false, false, true); + $DisplayName = Artists::display_artists($ExtendedArtists, false, false, false); foreach ($ExtendedArtists[1] as $Artist) { $JsonArtists[] = array( - 'id' => (int)$Artist['id'], - 'name' => $Artist['name'], - 'aliasid' => (int)$Artist['id'] - ); - } - } elseif (!empty($Artists)) { - $DisplayName = Artists::display_artists(array(1 => $Artists), false, false, true); - foreach ($Artists as $Artist) { - $JsonArtists[] = array( - 'id' => (int)$Artist['id'], - 'name' => $Artist['name'], - 'aliasid' => (int)$Artist['id'] - ); + 'id' => (int)$Artist['id'], + 'name' => $Artist['name'], + 'aliasid' => (int)$Artist['aliasid']); } } else { $DisplayName = ''; @@ -551,11 +90,6 @@ foreach ($Torrents as $TorrentID => $Data) { // All of the individual torrents in the group - // If they're using the advanced search and have chosen enabled grouping, we just skip the torrents that don't check out - if (!isset($TorrentIDs[$TorrentID])) { - continue; - } - if ($Data['Remastered'] && !$Data['RemasterYear']) { $FirstUnknown = !isset($FirstUnknown); } @@ -682,11 +216,7 @@ ); } } - -echo json_encode( - array( - 'status' => 'success', - 'response' => array( - 'currentPage' => intval($Page), - 'pages' => ceil($NumResults / TORRENTS_PER_PAGE), - 'results' => $JsonGroups))); +json_print('success', array( + 'currentPage' => intval($Page), + 'pages' => ceil($NumResults / TORRENTS_PER_PAGE), + 'results' => $JsonGroups)); diff --git a/sections/ajax/info.php b/sections/ajax/info.php index 6af1c11d..103d6877 100644 --- a/sections/ajax/info.php +++ b/sections/ajax/info.php @@ -93,7 +93,7 @@ // Subscriptions $NewSubscriptions = Subscriptions::has_new_subscriptions(); -json_die("success", array( +json_print("success", array( 'username' => $LoggedUser['Username'], 'id' => (int)$LoggedUser['ID'], 'authkey' => $LoggedUser['AuthKey'], diff --git a/sections/ajax/news_ajax.php b/sections/ajax/news_ajax.php index 1a480e66..6e8e7bcb 100644 --- a/sections/ajax/news_ajax.php +++ b/sections/ajax/news_ajax.php @@ -37,4 +37,4 @@ ); } -json_die('success', json_encode($NewsResponse)); +json_print('success', json_encode($NewsResponse)); diff --git a/sections/ajax/notifications.php b/sections/ajax/notifications.php index be5e1ee8..65bbb980 100644 --- a/sections/ajax/notifications.php +++ b/sections/ajax/notifications.php @@ -99,7 +99,7 @@ } } -json_die("success", array( +json_print("success", array( 'currentPages' => intval($Page), 'pages' => ceil($TorrentCount / NOTIFICATIONS_PER_PAGE), 'numNew' => $NumNew, diff --git a/sections/ajax/request.php b/sections/ajax/request.php index 077f5a86..a844f2ca 100644 --- a/sections/ajax/request.php +++ b/sections/ajax/request.php @@ -112,7 +112,7 @@ foreach ($Request['Tags'] as $Tag) { $JsonTags[] = $Tag; } -json_die('success', array( +json_print('success', array( 'requestId' => (int)$RequestID, 'requestorId' => (int)$Request['UserID'], 'requestorName' => $Requestor['Username'], diff --git a/sections/ajax/requests.php b/sections/ajax/requests.php index c52e51e0..1add4ec1 100644 --- a/sections/ajax/requests.php +++ b/sections/ajax/requests.php @@ -321,7 +321,7 @@ } if ($NumResults == 0) { - json_die("success", array( + json_print("success", array( 'currentPage' => 1, 'pages' => 1, 'results' => array() @@ -381,7 +381,7 @@ ); } - json_die("success", array( + json_print("success", array( 'currentPage' => intval($Page), 'pages' => ceil($NumResults / REQUESTS_PER_PAGE), 'results' => $JsonResults diff --git a/sections/ajax/stats.php b/sections/ajax/stats.php index 13d3c71e..2ab24ae5 100644 --- a/sections/ajax/stats.php +++ b/sections/ajax/stats.php @@ -1,7 +1,133 @@ <? -if (in_array($_GET['stat'], array('inbox', 'uploads', 'bookmarks', 'notifications', 'subscriptions', 'comments', 'friends'))) { - $Cache->begin_transaction('stats_links'); - $Cache->update_row(false, array($_GET['stat'] => '+1')); - $Cache->commit_transaction(0); +// Begin user stats +if (($UserCount = $Cache->get_value('stats_user_count')) === false) { + $DB->query(" + SELECT COUNT(ID) + FROM users_main + WHERE Enabled = '1'"); + list($UserCount) = $DB->next_record(); + $Cache->cache_value('stats_user_count', $UserCount, 0); //inf cache } + +if (($UserStats = $Cache->get_value('stats_users')) === false) { + $DB->query(" + SELECT COUNT(ID) + FROM users_main + WHERE Enabled = '1' + AND LastAccess > '".time_minus(3600 * 24)."'"); + list($UserStats['Day']) = $DB->next_record(); + + $DB->query(" + SELECT COUNT(ID) + FROM users_main + WHERE Enabled = '1' + AND LastAccess > '".time_minus(3600 * 24 * 7)."'"); + list($UserStats['Week']) = $DB->next_record(); + + $DB->query(" + SELECT COUNT(ID) + FROM users_main + WHERE Enabled = '1' + AND LastAccess > '".time_minus(3600 * 24 * 30)."'"); + list($UserStats['Month']) = $DB->next_record(); + + $Cache->cache_value('stats_users', $UserStats, 0); +} + +// Begin torrent stats +if (($TorrentCount = $Cache->get_value('stats_torrent_count')) === false) { + $DB->query(" + SELECT COUNT(ID) + FROM torrents"); + list($TorrentCount) = $DB->next_record(); + $Cache->cache_value('stats_torrent_count', $TorrentCount, 604800); // staggered 1 week cache +} + +if (($AlbumCount = $Cache->get_value('stats_album_count')) === false) { + $DB->query(" + SELECT COUNT(ID) + FROM torrents_group + WHERE CategoryID = '1'"); + list($AlbumCount) = $DB->next_record(); + $Cache->cache_value('stats_album_count', $AlbumCount, 604830); // staggered 1 week cache +} + +if (($ArtistCount = $Cache->get_value('stats_artist_count')) === false) { + $DB->query(" + SELECT COUNT(ArtistID) + FROM artists_group"); + list($ArtistCount) = $DB->next_record(); + $Cache->cache_value('stats_artist_count', $ArtistCount, 604860); // staggered 1 week cache +} + +if (($PerfectCount = $Cache->get_value('stats_perfect_count')) === false) { + $DB->query(" + SELECT COUNT(ID) + FROM torrents + WHERE ((LogScore = 100 AND Format = 'FLAC') + OR (Media = 'Vinyl' AND Format = 'FLAC') + OR (Media = 'WEB' AND Format = 'FLAC') + OR (Media = 'DVD' AND Format = 'FLAC') + OR (Media = 'Soundboard' AND Format = 'FLAC') + )"); + list($PerfectCount) = $DB->next_record(); + $Cache->cache_value('stats_perfect_count', $PerfectCount, 604890); // staggered 1 week cache +} + +// Begin request stats +if (($RequestStats = $Cache->get_value('stats_requests')) === false) { + $DB->query(" + SELECT COUNT(ID) + FROM requests"); + list($RequestCount) = $DB->next_record(); + $DB->query(" + SELECT COUNT(ID) + FROM requests + WHERE FillerID > 0"); + list($FilledCount) = $DB->next_record(); + $Cache->cache_value('stats_requests', array($RequestCount, $FilledCount), 11280); +} else { + list($RequestCount, $FilledCount) = $RequestStats; +} + + +// Begin swarm stats +if (($PeerStats = $Cache->get_value('stats_peers')) === false) { + //Cache lock! + if ($Cache->get_query_lock('peer_stats')) { + $DB->query(" + SELECT IF(remaining=0,'Seeding','Leeching') AS Type, COUNT(uid) + FROM xbt_files_users + WHERE active = 1 + GROUP BY Type"); + $PeerCount = $DB->to_array(0, MYSQLI_NUM, false); + $LeecherCount = isset($PeerCount['Leeching']) ? $PeerCount['Leeching'][1] : 0; + $SeederCount = isset($PeerCount['Seeding']) ? $PeerCount['Seeding'][1] : 0; + $Cache->cache_value('stats_peers', array($LeecherCount, $SeederCount), 1209600); // 2 week cache + $Cache->clear_query_lock('peer_stats'); + } else { + $LeecherCount = $SeederCount = 0; + } +} else { + list($LeecherCount, $SeederCount) = $PeerStats; +} + +json_print("success", array( + 'maxUsers' => USER_LIMIT, + 'enabledUsers' => (int) $UserCount, + 'usersActiveThisDay' => (int) $UserStats['Day'], + 'usersActiveThisWeek' => (int) $UserStats['Week'], + 'usersActiveThisMonth' => (int) $UserStats['Month'], + + 'torrentCount' => (int) $TorrentCount, + 'releaseCount' => (int) $AlbumCount, + 'artistCount' => (int) $ArtistCount, + 'perfectFlacCount' => (int) $PerfectCount, + + 'requestCount' => (int) $RequestCount, + 'filledRequestCount' => (int) $FilledCount, + + 'seederCount' => (int) $SeederCount, + 'leecherCount' => (int) $LeecherCount +)); ?> diff --git a/sections/ajax/subscriptions.php b/sections/ajax/subscriptions.php index 98aea2f4..c8daeeb5 100644 --- a/sections/ajax/subscriptions.php +++ b/sections/ajax/subscriptions.php @@ -86,7 +86,7 @@ $JsonPosts[] = $JsonPost; } -json_die('success', array( +json_print('success', array( 'threads' => $JsonPosts )); ?> diff --git a/sections/ajax/tcomments.php b/sections/ajax/tcomments.php index d167cf5b..9dc513f7 100644 --- a/sections/ajax/tcomments.php +++ b/sections/ajax/tcomments.php @@ -31,7 +31,7 @@ ); } -json_die("success", array( +json_print("success", array( 'page' => (int)$Page, 'pages' => ceil($NumComments / TORRENT_COMMENTS_PER_PAGE), 'comments' => $JsonComments diff --git a/sections/ajax/torrent.php b/sections/ajax/torrent.php index dedafeea..f6dd87cb 100644 --- a/sections/ajax/torrent.php +++ b/sections/ajax/torrent.php @@ -128,4 +128,4 @@ 'username' => $Userinfo['Username'] ); -json_die("success", array('group' => $JsonTorrentDetails, 'torrent' => array_pop($JsonTorrentList))); +json_print("success", array('group' => $JsonTorrentDetails, 'torrent' => array_pop($JsonTorrentList))); diff --git a/sections/ajax/torrentgroup.php b/sections/ajax/torrentgroup.php index e95658cd..bbf9974e 100644 --- a/sections/ajax/torrentgroup.php +++ b/sections/ajax/torrentgroup.php @@ -118,4 +118,4 @@ ); } -json_die("success", array('group' => $JsonTorrentDetails, 'torrents' => $JsonTorrentList)); +json_print("success", array('group' => $JsonTorrentDetails, 'torrents' => $JsonTorrentList)); diff --git a/sections/ajax/user.php b/sections/ajax/user.php index 702a6d6f..1ef89abb 100644 --- a/sections/ajax/user.php +++ b/sections/ajax/user.php @@ -101,8 +101,10 @@ function check_paranoia_here($Setting) { WHERE UserID = '$UserID'"); list($Uploads) = $DB->next_record(); } else { - $RequestsVoted = 0; - $TotalSpent = 0; + $RequestsFilled = null; + $TotalBounty = null; + $RequestsVoted = null; + $TotalSpent = null; } if (check_paranoia_here('uploads+')) { $DB->query(" @@ -121,7 +123,7 @@ function check_paranoia_here($Setting) { WHERE UserID = $UserID"); list($ArtistsAdded) = $DB->next_record(); } else { - $ArtistsAdded = 0; + $ArtistsAdded = null; } // Do the ranks. @@ -189,6 +191,33 @@ function check_paranoia_here($Setting) { list($NumComments) = $DB->next_record(); } +if (check_paranoia_here('torrentcomments+')) { + $DB->query(" + SELECT COUNT(ID) + FROM comments + WHERE Page = 'artist' + AND AuthorID = '$UserID'"); + list($NumArtistComments) = $DB->next_record(); +} + +if (check_paranoia_here('torrentcomments+')) { + $DB->query(" + SELECT COUNT(ID) + FROM comments + WHERE Page = 'collages' + AND AuthorID = '$UserID'"); + list($NumCollageComments) = $DB->next_record(); +} + +if (check_paranoia_here('torrentcomments+')) { + $DB->query(" + SELECT COUNT(ID) + FROM comments + WHERE Page = 'requests' + AND AuthorID = '$UserID'"); + list($NumRequestComments) = $DB->next_record(); +} + if (check_paranoia_here('collages+')) { $DB->query(" SELECT COUNT(ID) @@ -304,7 +333,7 @@ function check_paranoia_here($Setting) { header('Content-Type: text/plain; charset=utf-8'); -json_die("success", array( +json_print("success", array( 'username' => $Username, 'avatar' => $Avatar, 'isFriend' => $Friend, @@ -338,18 +367,24 @@ function check_paranoia_here($Setting) { ), 'community' => array( 'posts' => (int)$ForumPosts, - 'torrentComments' => (int)$NumComments, + 'torrentComments' => (($NumComments == null) ? null : (int)$NumComments), + 'artistComments' => (($NumArtistComments == null) ? null : (int)$NumArtistComments), + 'collageComments' => (($NumCollageComments == null) ? null : (int)$NumCollageComments), + 'requestComments' => (($NumRequestComments == null) ? null : (int)$NumRequestComments), 'collagesStarted' => (($NumCollages == null) ? null : (int)$NumCollages), 'collagesContrib' => (($NumCollageContribs == null) ? null : (int)$NumCollageContribs), 'requestsFilled' => (($RequestsFilled == null) ? null : (int)$RequestsFilled), + 'bountyEarned' => (($TotalBounty == null) ? null : (int)$TotalBounty), 'requestsVoted' => (($RequestsVoted == null) ? null : (int)$RequestsVoted), + 'bountySpent' => (($TotalSpent == null) ? null : (int)$TotalSpent), 'perfectFlacs' => (($PerfectFLACs == null) ? null : (int)$PerfectFLACs), 'uploaded' => (($Uploads == null) ? null : (int)$Uploads), 'groups' => (($UniqueGroups == null) ? null : (int)$UniqueGroups), 'seeding' => (($Seeding == null) ? null : (int)$Seeding), 'leeching' => (($Leeching == null) ? null : (int)$Leeching), 'snatched' => (($Snatched == null) ? null : (int)$Snatched), - 'invited' => (($Invited == null) ? null : (int)$Invited) + 'invited' => (($Invited == null) ? null : (int)$Invited), + 'artistsAdded' => (($ArtistsAdded == null) ? null : (int)$ArtistsAdded) ) )); ?> diff --git a/sections/ajax/user_recents.php b/sections/ajax/user_recents.php index c81ed1e8..fc22b979 100644 --- a/sections/ajax/user_recents.php +++ b/sections/ajax/user_recents.php @@ -62,7 +62,7 @@ $Results['uploads'] = "hidden"; } -json_die("success", $Results); +json_print("success", $Results); function check_paranoia_here($Setting) { global $Paranoia, $Class, $UserID, $Preview; diff --git a/sections/ajax/usersearch.php b/sections/ajax/usersearch.php index 98e6cc7f..249f992f 100644 --- a/sections/ajax/usersearch.php +++ b/sections/ajax/usersearch.php @@ -51,7 +51,7 @@ ); } -json_die("success", array( +json_print("success", array( 'currentPage' => (int)$Page, 'pages' => ceil($NumResults / USERS_PER_PAGE), 'results' => $JsonUsers diff --git a/sections/ajax/wiki.php b/sections/ajax/wiki.php index 62153945..9c3b1d5e 100644 --- a/sections/ajax/wiki.php +++ b/sections/ajax/wiki.php @@ -23,7 +23,7 @@ Text::$TOC = true; $TextBody = Text::full_format($Body, false); -json_die("success", array( +json_print("success", array( 'title' => $Title, 'bbBody' => $Body, 'body' => $TextBody, diff --git a/sections/artist/concerts.php b/sections/artist/concerts.php index 99cb3929..8bd75c7e 100644 --- a/sections/artist/concerts.php +++ b/sections/artist/concerts.php @@ -16,7 +16,7 @@ make_concert_link($Event); } } else { // Single event - make_concert_link($ArtistEvents['events']['event']); + make_concert_link($ArtistEvents['events']['event'], $Name); } echo '</ul>'; } @@ -34,7 +34,7 @@ </div> <? -function make_concert_link($Event) { +function make_concert_link($Event, $Name) { // The event doesn't have a start date (this should never happen) if ($Event['startDate'] == '') { return; @@ -46,7 +46,7 @@ function make_concert_link($Event) { ?> <form class="hidden" action="" id="concert<?=$Event['id']?>" method="post"> <input type="hidden" name="action" value="concert_thread" /> - <input type="hidden" name="concert_title" value="<?='[Concert] ' . display_str($Name) . " - $ConcertTitle"?>" /> + <input type="hidden" name="concert_title" value="<?='[Concert] ' . $Event['artists']['artist'] . " - $ConcertTitle"?>" /> <input type="hidden" name="concert_id" value="<?=$Event['id']?>" /> <input type="hidden" name="concert_template" value="<?=get_concert_post_template($Name, $Event)?>" /> </form> @@ -80,7 +80,7 @@ function get_concert_post_template($Artist, $Event) { if (strpos ($Url, '://') === false) { $Url = 'http://' . $Url; } - $Website = '[b]Web site:[/b] ' . $Url; + $Website = '[b]Website:[/b] ' . $Url; } if (isset($Event['artists']['artist']) && (count($Event['artists']['artist']) === 1 && strtolower($Event['artists']['artist'][1]) == strtolower($Artist))) { $i = 0; diff --git a/sections/artist/download.php b/sections/artist/download.php index 51702bde..7ec57041 100644 --- a/sections/artist/download.php +++ b/sections/artist/download.php @@ -81,8 +81,14 @@ if (!$DB->has_results()) { error(404); } -$Releases = $DB->to_array('GroupID', MYSQLI_ASSOC, false); -$GroupIDs = array_keys($Releases); +$ArtistRoles = array(); +while (list($GroupID, $Importance) = $DB->next_record(MYSQLI_NUM, false)) { + // Get the highest importances to place the .torrents in the most relevant folders + if (!isset($ArtistRoles[$GroupID]) || $Importance < $ArtistRoles[$GroupID]) { + $ArtistRoles[$GroupID] = $Importance; + } +} +$GroupIDs = array_keys($ArtistRoles); $SQL = 'SELECT CASE '; @@ -163,12 +169,40 @@ $Collector->skip_file($Download); continue; } - if ($Releases[$GroupID]['Importance'] == 1) { - $ReleaseTypeName = $ReleaseTypes[$Download['ReleaseType']]; - } elseif ($Releases[$GroupID]['Importance'] == 2) { - $ReleaseTypeName = 'Guest Appearance'; - } elseif ($Releases[$GroupID]['Importance'] == 3) { - $ReleaseTypeName = 'Remixed By'; + switch ($ArtistRoles[$GroupID]) { + /** + * 1 => Main artist + * 2 => Guest artist + * 3 => Remixer + * 4 => Composer + * 5 => Conductor + * 6 => DJ / Compiler + * 7 => Producer + */ + case '1': + $ReleaseTypeName = $ReleaseTypes[$Download['ReleaseType']]; + break; + case '2': + $ReleaseTypeName = 'Guest Appearance'; + break; + case '3': + $ReleaseTypeName = 'Remixed By'; + break; + case '4': + $ReleaseTypeName = 'Composition'; + break; + case '5': + $ReleaseTypeName = 'Conducted By'; + break; + case '6': + $ReleaseTypeName = 'DJ Mix'; + break; + case '7': + $ReleaseTypeName = 'Produced By'; + break; + default: + $ReleaseTypeName = 'Other'; + break; } $Collector->add_file($TorrentFile, $Download, $ReleaseTypeName); unset($Download); diff --git a/sections/login/index.php b/sections/login/index.php index 72825d33..630324d5 100644 --- a/sections/login/index.php +++ b/sections/login/index.php @@ -30,21 +30,22 @@ // Recover password if (!empty($_REQUEST['key'])) { // User has entered a new password, use step 2 + $DB->query(" SELECT m.ID, m.Email, m.ipcc, i.ResetExpires - FROM users_main AS m + FROM users_main as m INNER JOIN users_info AS i ON i.UserID = m.ID WHERE i.ResetKey = '".db_string($_REQUEST['key'])."' AND i.ResetKey != '' AND m.Enabled = '1'"); list($UserID, $Email, $Country, $Expires) = $DB->next_record(); - if ($UserID && strtotime($Expires) > time()) { - // If the user has requested a password change, and his key has not expired + + // If the user has requested a password change, and his key has not expired $Validate->SetFields('password', '1', 'regex', 'You entered an invalid password. A strong password is 8 characters or longer, contains at least 1 lowercase and uppercase letter, contains at least a number or symbol', array('regex' => '/(?=^.{8,}$)(?=.*[^a-zA-Z])(?=.*[A-Z])(?=.*[a-z]).*$/')); $Validate->SetFields('verifypassword', '1', 'compare', 'Your passwords did not match.', array('comparefield' => 'password')); diff --git a/sections/requests/take_fill.php b/sections/requests/take_fill.php index 3a19a103..c2f229e6 100644 --- a/sections/requests/take_fill.php +++ b/sections/requests/take_fill.php @@ -219,5 +219,8 @@ $SphQL = new SphinxqlQuery(); $SphQL->raw_query("UPDATE requests, requests_delta SET torrentid = $TorrentID, fillerid = $FillerID WHERE id = $RequestID", false); + + + header("Location: requests.php?action=view&id=$RequestID"); ?> diff --git a/sections/schedule/index.php b/sections/schedule/index.php index 480fa2c1..69cc14dc 100644 --- a/sections/schedule/index.php +++ b/sections/schedule/index.php @@ -825,7 +825,7 @@ function next_hour() { GROUP BY um.ID"); while (list($Username, $Email) = $DB->next_record()) { $Body = "Hi $Username,\n\nIt has been almost 4 months since you used your account at ".site_url().". This is an automated email to inform you that your account will be disabled in 10 days if you do not sign in."; - Misc::send_email($Email, 'Your '.SITE_NAME.' account is about to be disabled', $Body); + Misc::send_email($Email, 'Your '.SITE_NAME.' account is about to be disabled', $Body, 'noreply'); } $DB->query(" @@ -878,22 +878,29 @@ function next_hour() { //------------- Demote users --------------------------------------------// sleep(10); + // Demote to Member when the ratio falls below 0.95 or they have less than 25 GB upload + $DemoteClasses = [POWER, ELITE, TORRENT_MASTER, POWER_TM, ELITE_TM]; $Query = $DB->query(' SELECT ID FROM users_main - WHERE PermissionID IN('.POWER.', '.ELITE.', '.TORRENT_MASTER.') - AND Uploaded / Downloaded < 0.95 - OR PermissionID IN('.POWER.', '.ELITE.', '.TORRENT_MASTER.') - AND Uploaded < 25 * 1024 * 1024 * 1024'); + WHERE PermissionID IN(' . implode(', ', $DemoteClasses) . ') + AND ( + Uploaded / Downloaded < 0.95 + OR Uploaded < 25 * 1024 * 1024 * 1024 + )'); echo "demoted 1\n"; $DB->query(' - UPDATE users_main - SET PermissionID = '.MEMBER.' - WHERE PermissionID IN('.POWER.', '.ELITE.', '.TORRENT_MASTER.') - AND Uploaded / Downloaded < 0.95 - OR PermissionID IN('.POWER.', '.ELITE.', '.TORRENT_MASTER.') - AND Uploaded < 25 * 1024 * 1024 * 1024'); + UPDATE users_info AS ui + JOIN users_main AS um ON um.ID = ui.UserID + SET + um.PermissionID = ' . MEMBER . ", + ui.AdminComment = CONCAT('" . sqltime() . ' - Class changed to ' . Users::make_class_string(MEMBER) . " by System\n\n', ui.AdminComment) + WHERE um.PermissionID IN (" . implode(', ', $DemoteClasses) . ') + AND ( + um.Uploaded / um.Downloaded < 0.95 + OR um.Uploaded < 25 * 1024 * 1024 * 1024 + )'); $DB->set_query_id($Query); while (list($UserID) = $DB->next_record()) { /*$Cache->begin_transaction("user_info_$UserID"); @@ -905,17 +912,22 @@ function next_hour() { } echo "demoted 2\n"; + // Demote to User when the ratio drops below 0.65 + $DemoteClasses = [MEMBER, POWER, ELITE, TORRENT_MASTER, POWER_TM, ELITE_TM]; $Query = $DB->query(' SELECT ID FROM users_main - WHERE PermissionID IN('.MEMBER.', '.POWER.', '.ELITE.', '.TORRENT_MASTER.') + WHERE PermissionID IN(' . implode(', ', $DemoteClasses) . ') AND Uploaded / Downloaded < 0.65'); echo "demoted 3\n"; $DB->query(' - UPDATE users_main - SET PermissionID = '.USER.' - WHERE PermissionID IN('.MEMBER.', '.POWER.', '.ELITE.', '.TORRENT_MASTER.') - AND Uploaded / Downloaded < 0.65'); + UPDATE users_info AS ui + JOIN users_main AS um ON um.ID = ui.UserID + SET + um.PermissionID = ' . USER . ", + ui.AdminComment = CONCAT('" . sqltime() . ' - Class changed to ' . Users::make_class_string(USER) . " by System\n\n', ui.AdminComment) + WHERE um.PermissionID IN (" . implode(', ', $DemoteClasses) . ') + AND um.Uploaded / um.Downloaded < 0.65'); $DB->set_query_id($Query); while (list($UserID) = $DB->next_record()) { /*$Cache->begin_transaction("user_info_$UserID"); diff --git a/sections/tools/development/update_geoip.php b/sections/tools/development/update_geoip.php index 6ddfaaa2..3cadf214 100644 --- a/sections/tools/development/update_geoip.php +++ b/sections/tools/development/update_geoip.php @@ -13,7 +13,7 @@ shell_exec('unzip GeoLiteCity-latest.zip'); shell_exec('rm GeoLiteCity-latest.zip'); -if (($Locations = file("GeoLiteCity_".date('Ym')."05/GeoLiteCity-Location.csv", FILE_IGNORE_NEW_LINES)) === false) { +if (($Locations = file("GeoLiteCity_".date('Ym')."02/GeoLiteCity-Location.csv", FILE_IGNORE_NEW_LINES)) === false) { error('Download or extraction of maxmind database failed'); } array_shift($Locations); @@ -32,7 +32,7 @@ echo 'There are '.count($CountryIDs).' CountryIDs'; echo '<br />'; -if (($Blocks = file("GeoLiteCity_".date('Ym')."05/GeoLiteCity-Blocks.csv", FILE_IGNORE_NEW_LINES)) === false) { +if (($Blocks = file("GeoLiteCity_".date('Ym')."02/GeoLiteCity-Blocks.csv", FILE_IGNORE_NEW_LINES)) === false) { echo 'Error'; } array_shift($Blocks); diff --git a/sections/tools/index.php b/sections/tools/index.php index 4fa8fa83..e633154c 100644 --- a/sections/tools/index.php +++ b/sections/tools/index.php @@ -168,15 +168,14 @@ $DB->query(" INSERT INTO news (UserID, Title, Body, Time) VALUES ('$LoggedUser[ID]', '".db_string($_POST['title'])."', '".db_string($_POST['body'])."', '".sqltime()."')"); + $Cache->delete_value('news_latest_id'); + $Cache->delete_value('news_latest_title'); + $Cache->delete_value('news'); NotificationsManager::send_push(NotificationsManager::get_push_enabled_users(), $_POST['title'], $_POST['body'], site_url() . 'index.php', NotificationsManager::NEWS); - $Cache->delete_value('news_latest_id'); - $Cache->delete_value('news_latest_title'); - $Cache->delete_value('news'); - header('Location: index.php'); break; diff --git a/sections/top10/index.php b/sections/top10/index.php index a2f13aad..7175f588 100644 --- a/sections/top10/index.php +++ b/sections/top10/index.php @@ -35,6 +35,7 @@ case 'lastfm': include(SERVER_ROOT.'/sections/top10/lastfm.php'); break; + default: error(404); break; diff --git a/sections/torrents/browse.php b/sections/torrents/browse.php index 5268bc7a..4dd1dc73 100644 --- a/sections/torrents/browse.php +++ b/sections/torrents/browse.php @@ -1,23 +1,4 @@ <? -/************************************************************************ -*-------------------- Browse page --------------------------------------- -* Welcome to one of the most complicated pages in all of gazelle - the -* browse page. -* -* This is the page that is displayed when someone visits torrents.php -* -* It offers normal and advanced search, as well as enabled/disabled -* grouping. -* -* Don't blink. -* Blink and you're dead. -* Don't turn your back. -* Don't look away. -* And don't blink. -* Good Luck. -* -*************************************************************************/ - include(SERVER_ROOT.'/sections/torrents/functions.php'); // The "order by x" links on columns headers @@ -35,7 +16,6 @@ function header_link($SortKey, $DefaultWay = 'desc') { return "torrents.php?order_way=$NewWay&order_by=$SortKey&".Format::get_url(array('order_way', 'order_by')); } -/** Start default parameters and validation **/ if (!empty($_GET['searchstr']) || !empty($_GET['groupname'])) { if (!empty($_GET['searchstr'])) { $InfoHash = $_GET['searchstr']; @@ -113,422 +93,29 @@ function header_link($SortKey, $DefaultWay = 'desc') { } } // Terms were not submitted via the search form -if (!isset($_GET['searchsubmit'])) { - $_GET['group_results'] = !$LoggedUser['DisableGrouping2']; -} - -if (isset($_GET['group_results']) && $_GET['group_results']) { - $_GET['group_results'] = 1; - $GroupResults = true; - $SortOrders = array( - // 'url attr' => [global order, order within group] - 'year' => array('year', 'year'), - 'time' => array('id', 'id'), - 'size' => array('maxsize', 'size'), - 'seeders' => array('sumseeders', 'seeders'), - 'leechers' => array('sumleechers', 'leechers'), - 'snatched' => array('sumsnatched', 'snatched'), - 'random' => false); - - $AggregateExp = array( - 'maxsize' => 'MAX(size) AS maxsize', - 'sumseeders' => 'SUM(seeders) AS sumseeders', - 'sumleechers' => 'SUM(leechers) AS sumleechers', - 'sumsnatched' => 'SUM(snatched) AS sumsnatched'); +if (isset($_GET['searchsubmit'])) { + $GroupResults = !empty($_GET['group_results']); } else { - $GroupResults = false; - $SortOrders = array( - 'year' => 'year', - 'time' => 'id', - 'size' => 'size', - 'seeders' => 'seeders', - 'leechers' => 'leechers', - 'snatched' => 'snatched', - 'random' => false); -} - -if (empty($_GET['order_by']) || !isset($SortOrders[$_GET['order_by']])) { - $_GET['order_by'] = 'time'; - $OrderBy = 'time'; // For header links -} else { - $OrderBy = $_GET['order_by']; + $GroupResults = !$LoggedUser['DisableGrouping2']; } if (!empty($_GET['order_way']) && $_GET['order_way'] == 'asc') { $OrderWay = 'asc'; } else { - $_GET['order_way'] = 'desc'; $OrderWay = 'desc'; } -/** End default parameters and validation **/ - -/** Start preparation of property arrays **/ -array_pop($Bitrates); // remove 'other' -$SearchBitrates = array_merge($Bitrates, array('v0', 'v1', 'v2', '24bit')); - -foreach ($SearchBitrates as $ID => $Val) { - $SearchBitrates[$ID] = strtolower($Val); -} -foreach ($Formats as $ID => $Val) { - $SearchFormats[$ID] = strtolower($Val); -} -/** End preparation of property arrays **/ - -/** Start query preparation **/ -$SphQL = new SphinxqlQuery(); -$SphQLTor = new SphinxqlQuery(); - -if ($OrderBy == 'random') { - $SphQL->select('id, groupid, categoryid') - ->order_by('RAND()', ''); - $Random = true; -} elseif ($GroupResults) { - $OrderProperties = $SortOrders[$OrderBy]; - $SphQL->select('groupid, categoryid' . (isset($AggregateExp[$OrderProperties[0]]) ? ', '.$AggregateExp[$OrderProperties[0]] : '')) - ->group_by('groupid') - ->order_by($OrderProperties[0], $OrderWay) - ->order_group_by($OrderProperties[1], $OrderWay); - +if (empty($_GET['order_by']) || !isset(TorrentSearch::$SortOrders[$_GET['order_by']])) { + $OrderBy = 'time'; // For header links } else { - $SphQL->select('id, groupid, categoryid') - ->order_by($SortOrders[$OrderBy], $OrderWay); -} -$SphQL->from('torrents, delta'); -$SphQLTor->select('id')->from('torrents, delta'); -/** End query preparation **/ - -/** Start building search query **/ -$Filtered = false; -$EnableNegation = false; // Sphinx needs at least one positive search condition to support the NOT operator - -// File list searches make use of the proximity operator to ensure that all keywords match the same file -if (!empty($_GET['filelist'])) { - $SearchString = trim($_GET['filelist']); - if ($SearchString !== '') { - $SearchString = '"'.Sphinxql::sph_escape_string($_GET['filelist']).'"~20'; - $SphQL->where_match($SearchString, 'filelist', false); - $SphQLTor->where_match($SearchString, 'filelist', false); - $EnableNegation = true; - $Filtered = true; - } + $OrderBy = $_GET['order_by']; } -// Collect all entered search terms to find out whether to enable the NOT operator -$SearchWords = array(); -foreach (array('artistname', 'groupname', 'recordlabel', 'cataloguenumber', - 'taglist', 'remastertitle', 'remasteryear', 'remasterrecordlabel', - 'remastercataloguenumber', 'encoding', 'format', 'media', 'description') as $Search) { - if (!empty($_GET[$Search])) { - $SearchString = trim($_GET[$Search]); - if ($SearchString !== '') { - $SearchWords[$Search] = array('include' => array(), 'exclude' => array()); - if ($Search == 'taglist') { - $SearchString = strtr($SearchString, '.', '_'); - $Words = explode(',', $SearchString); - } else { - $Words = explode(' ', $SearchString); - } - foreach ($Words as $Word) { - $Word = trim($Word); - // Skip isolated hyphens to enable "Artist - Title" searches - if ($Word === '-') { - continue; - } - if ($Word[0] === '!' && strlen($Word) >= 2) { - if (strpos($Word, '!', 1) === false) { - $SearchWords[$Search]['exclude'][] = $Word; - } else { - $SearchWords[$Search]['include'][] = $Word; - $EnableNegation = true; - } - } elseif ($Word !== '') { - $SearchWords[$Search]['include'][] = $Word; - $EnableNegation = true; - } - } - } - } -} - -//Simple search -if (!empty($_GET['searchstr'])) { - $SearchString = trim($_GET['searchstr']); - $Words = explode(' ', strtolower($SearchString)); - if (!empty($Words)) { - $FilterBitrates = $FilterFormats = array(); - $BasicSearch = array('include' => array(), 'exclude' => array()); - foreach ($Words as $Word) { - $Word = trim($Word); - // Skip isolated hyphens to enable "Artist - Title" searches - if ($Word === '-') { - continue; - } - if ($Word[0] === '!' && strlen($Word) >= 2) { - if ($Word === '!100%') { - $_GET['haslog'] = '-1'; - } elseif (strpos($Word, '!', 1) === false) { - $BasicSearch['exclude'][] = $Word; - } else { - $BasicSearch['include'][] = $Word; - $EnableNegation = true; - } - } elseif (in_array($Word, $SearchBitrates)) { - $FilterBitrates[] = $Word; - $EnableNegation = true; - } elseif (in_array($Word, $SearchFormats)) { - $FilterFormats[] = $Word; - $EnableNegation = true; - } elseif ($Word === '100%') { - $_GET['haslog'] = '100'; - } elseif ($Word !== '') { - $BasicSearch['include'][] = $Word; - $EnableNegation = true; - } - } - if (!$EnableNegation && !empty($BasicSearch['exclude'])) { - $BasicSearch['include'] = array_merge($BasicSearch['include'], $BasicSearch['exclude']); - unset($BasicSearch['exclude']); - } - $QueryParts = array(); - foreach ($BasicSearch['include'] as $Word) { - $QueryParts[] = Sphinxql::sph_escape_string($Word); - } - if (!empty($BasicSearch['exclude'])) { - foreach ($BasicSearch['exclude'] as $Word) { - $QueryParts[] = '!'.Sphinxql::sph_escape_string(substr($Word, 1)); - } - } - if (!empty($FilterBitrates)) { - $SearchString = implode(' ', $FilterBitrates); - $SphQL->where_match($SearchString, 'encoding', false); - $SphQLTor->where_match($SearchString, 'encoding', false); - $Filtered = true; - } - if (!empty($FilterFormats)) { - $SearchString = implode(' ', $FilterFormats); - $SphQL->where_match($SearchString, 'format', false); - $SphQLTor->where_match($SearchString, 'format', false); - $Filtered = true; - } - if (!empty($QueryParts)) { - $SearchString = implode(' ', $QueryParts); - $SphQL->where_match($SearchString, '(groupname,artistname,yearfulltext)', false); - $SphQLTor->where_match($SearchString, '(groupname,artistname,yearfulltext)', false); - $Filtered = true; - } - } -} - -// Tag list -if (!empty($SearchWords['taglist'])) { - $_GET['tags_type'] = (!isset($_GET['tags_type']) || $_GET['tags_type'] == 1) ? '1' : '0'; - $TagType = (int)$_GET['tags_type']; - - //Get tags - $Tags = $SearchWords['taglist']; - - $TagFilter = Tags::tag_filter_sph($Tags, $EnableNegation, $TagType); - $TagListString = $TagFilter['input']; - - if (!empty($TagFilter['predicate'])) { - $SphQL->where_match($TagFilter['predicate'], 'taglist', false); - $SphQLTor->where_match($TagFilter['predicate'], 'taglist', false); - $Filtered = true; - } - unset($SearchWords['taglist']); - -} -elseif (!isset($_GET['tags_type'])) { - $_GET['tags_type'] = '1'; -} - -foreach ($SearchWords as $Search => $Words) { - $QueryParts = array(); - if (!$EnableNegation && !empty($Words['exclude'])) { - $Words['include'] = array_merge($Words['include'], $Words['exclude']); - unset($Words['exclude']); - } - foreach ($Words['include'] as $Word) { - $QueryParts[] = Sphinxql::sph_escape_string($Word); - } - if (!empty($Words['exclude'])) { - foreach ($Words['exclude'] as $Word) { - $QueryParts[] = '!'.Sphinxql::sph_escape_string(substr($Word, 1)); - } - } - if (!empty($QueryParts)) { - $SearchString = implode(' ', $QueryParts); - $SphQL->where_match($SearchString, $Search, false); - $SphQLTor->where_match($SearchString, $Search, false); - $Filtered = true; - } -} - -if (!empty($_GET['year'])) { - $Years = explode('-', $_GET['year']); - if (is_number($Years[0]) || (empty($Years[0]) && !empty($Years[1]) && is_number($Years[1]))) { - if (count($Years) === 1) { - $SphQL->where('year', $Years[0]); - $SphQLTor->where('year', $Years[0]); - } else { - if (empty($Years[0])) { - $SphQL->where_lt('year', $Years[1], true); - $SphQLTor->where_lt('year', $Years[1], true); - } elseif (empty($Years[1]) || !is_number($Years[1])) { - $SphQL->where_gt('year', $Years[0], true); - $SphQLTor->where_gt('year', $Years[0], true); - } else { - if ($Years[0] > $Years[1]) { - $Years = array_reverse($Years); - } - $SphQL->where_between('year', array($Years[0], $Years[1])); - $SphQLTor->where_between('year', array($Years[0], $Years[1])); - } - } - $Filtered = true; - } -} - -if (isset($_GET['haslog']) && $_GET['haslog'] !== '') { - if ($_GET['haslog'] === '100') { - $SphQL->where('logscore', 100); - $SphQLTor->where('logscore', 100); - } elseif ($_GET['haslog'] < 0) { - // Look for torrents with log score < 100 - $SphQL->where_lt('logscore', 100); - $SphQL->where('haslog', 1); - $SphQLTor->where_lt('logscore', 100); - $SphQLTor->where('haslog', 1); - } elseif ($_GET['haslog'] == 0) { - $SphQL->where('haslog', 0); - $SphQLTor->where('haslog', 0); - } else { - $SphQL->where('haslog', 1); - $SphQLTor->where('haslog', 1); - } - $Filtered = true; -} - -foreach (array('hascue', 'scene', 'vanityhouse', 'releasetype') as $Search) { - if (isset($_GET[$Search]) && $_GET[$Search] !== '') { - $SphQL->where($Search, $_GET[$Search]); - // Release type is group specific - if ($Search != 'releasetype') { - $SphQLTor->where($Search, $_GET[$Search]); - } - $Filtered = true; - } -} - -if (isset($_GET['freetorrent']) && $_GET['freetorrent'] !== '') { - switch ($_GET['freetorrent']) { - case 0: // Only normal freeleech - $SphQL->where('freetorrent', 0); - $SphQLTor->where('freetorrent', 0); - $Filtered = true; - break; - case 1: // Only free leech - $SphQL->where('freetorrent', 1); - $SphQLTor->where('freetorrent', 1); - $Filtered = true; - break; - case 2: // Only neutral leech - $SphQL->where('freetorrent', 2); - $SphQLTor->where('freetorrent', 2); - $Filtered = true; - break; - case 3: // Free or neutral leech - $SphQL->where('freetorrent', 0, true); - $SphQLTor->where('freetorrent', 0, true); - $Filtered = true; - break; - default: - unset($_GET['freetorrent']); - break; - } -} - -if (!empty($_GET['filter_cat'])) { - $SphQL->where('categoryid', array_keys($_GET['filter_cat'])); - $Filtered = true; -} -/** End building search query **/ - -/** Run search query and collect results **/ -if (isset($Random) && $GroupResults) { - // ORDER BY RAND() can't be used together with GROUP BY, so we need some special tactics - $Page = 1; - $SphQL->limit(0, 5 * TORRENTS_PER_PAGE, 5 * TORRENTS_PER_PAGE); - $SphQLResult = $SphQL->query(); - $TotalCount = $SphQLResult->get_meta('total_found'); - $Results = $SphQLResult->to_array('groupid'); - $GroupIDs = array_keys($Results); - $GroupCount = count($GroupIDs); - while ($SphQLResult->get_meta('total') < $TotalCount && $GroupCount < TORRENTS_PER_PAGE) { - // Make sure we get TORRENTS_PER_PAGE results, or all of them if there are less than TORRENTS_PER_PAGE hits - $SphQL->where('groupid', $GroupIDs, true); - $SphQLResult = $SphQL->query(); - if (!$SphQLResult->has_results()) { - break; - } - $Results += $SphQLResult->to_array('groupid'); - $GroupIDs = array_keys($Results); - $GroupCount = count($GroupIDs); - } - if ($GroupCount > TORRENTS_PER_PAGE) { - $Results = array_slice($Results, 0, TORRENTS_PER_PAGE, true); - } - $GroupIDs = array_keys($Results); - $NumResults = count($Results); -} else { - if (!empty($_GET['page']) && is_number($_GET['page']) && $_GET['page'] > 0) { - if (check_perms('site_search_many')) { - $Page = $_GET['page']; - } else { - $Page = min(SPHINX_MAX_MATCHES / TORRENTS_PER_PAGE, $_GET['page']); - } - $Offset = ($Page - 1) * TORRENTS_PER_PAGE; - $SphQL->limit($Offset, TORRENTS_PER_PAGE, $Offset + TORRENTS_PER_PAGE); - } else { - $Page = 1; - $SphQL->limit(0, TORRENTS_PER_PAGE, TORRENTS_PER_PAGE); - } - $SphQLResult = $SphQL->query(); - $NumResults = $SphQLResult->get_meta('total_found'); - if ($GroupResults) { - $Results = $SphQLResult->to_array('groupid'); - $GroupIDs = array_keys($Results); - } else { - $Results = $SphQLResult->to_array('id'); - $GroupIDs = $SphQLResult->collect('groupid'); - } -} - -if (!check_perms('site_search_many') && $NumResults > SPHINX_MAX_MATCHES) { - $NumResults = SPHINX_MAX_MATCHES; -} - -if ($NumResults) { - $Groups = Torrents::get_groups($GroupIDs); - - if (!empty($Groups) && $GroupResults) { - $TorrentIDs = array(); - foreach ($Groups as $Group) { - if (!empty($Group['Torrents'])) { - $TorrentIDs = array_merge($TorrentIDs, array_keys($Group['Torrents'])); - } - } - $TorrentCount = count($TorrentIDs); - if ($TorrentCount > 0) { - // Get a list of all torrent ids that match the search query - $SphQLTor->where('id', $TorrentIDs)->limit(0, $TorrentCount, $TorrentCount); - $SphQLResultTor = $SphQLTor->query(); - $TorrentIDs = $SphQLResultTor->to_pair('id', 'id'); // Because isset() is faster than in_array() - } - } -} -/** End run search query and collect results **/ +$Page = !empty($_GET['page']) ? (int) $_GET['page'] : 1; +$Search = new TorrentSearch($GroupResults, $OrderBy, $OrderWay, $Page, TORRENTS_PER_PAGE); +$Results = $Search->query($_GET); +$Groups = $Search->get_groups(); +$NumResults = $Search->record_count(); $HideFilter = isset($LoggedUser['ShowTorFilter']) && $LoggedUser['ShowTorFilter'] == 0; // This is kinda ugly, but the enormous if paragraph was really hard to read @@ -715,7 +302,7 @@ function header_link($SortKey, $DefaultWay = 'desc') { <tr id="tagfilter"> <td class="label"><span title="Use !tag to exclude tag" class="tooltip">Tags (comma-separated):</span></td> <td colspan="3" class="ft_taglist"> - <input type="search" size="40" id="tags" name="taglist" class="inputtext smaller" value="<?=!empty($TagListString) ? display_str($TagListString) : ''?>"<? Users::has_autocomplete_enabled('other'); ?> />  + <input type="search" size="40" id="tags" name="taglist" class="inputtext smaller" value="<?=display_str($Search->get_terms('taglist'))?>"<? Users::has_autocomplete_enabled('other'); ?> />  <input type="radio" name="tags_type" id="tags_type0" value="0"<?Format::selected('tags_type', 0, 'checked')?> /><label for="tags_type0"> Any</label>   <input type="radio" name="tags_type" id="tags_type1" value="1"<?Format::selected('tags_type', 1, 'checked')?> /><label for="tags_type1"> All</label> </td> @@ -743,7 +330,7 @@ function header_link($SortKey, $DefaultWay = 'desc') { <label for="group_results">Group by release:</label> </td> <td colspan="3" class="ft_group_results"> - <input type="checkbox" value="1" name="group_results" id="group_results"<?Format::selected('group_results', 1, 'checked')?> /> + <input type="checkbox" value="1" name="group_results" id="group_results"<?=$GroupResults ? ' checked="checked"' : ''?> /> </td> </tr> </table> @@ -818,7 +405,7 @@ function header_link($SortKey, $DefaultWay = 'desc') { <input type="hidden" name="searchsubmit" value="1" /> <input type="button" value="Reset" onclick="location.href = 'torrents.php<? if (isset($_GET['action']) && $_GET['action'] === 'advanced') { ?>?action=advanced<? } ?>'" />    -<? if ($Filtered) { ?> +<? if ($Search->has_filters()) { ?> <input type="submit" name="setdefault" value="Make default" /> <? } @@ -913,13 +500,12 @@ function header_link($SortKey, $DefaultWay = 'desc') { <? // Start printing torrent list -foreach ($Results as $Result) { - $GroupID = $Result['groupid']; +foreach ($Results as $Key => $GroupID) { $GroupInfo = $Groups[$GroupID]; if (empty($GroupInfo['Torrents'])) { continue; } - $CategoryID = $Result['categoryid']; + $CategoryID = $GroupInfo['CategoryID']; $GroupYear = $GroupInfo['Year']; $ExtendedArtists = $GroupInfo['ExtendedArtists']; $GroupCatalogueNumber = $GroupInfo['CatalogueNumber']; @@ -930,16 +516,15 @@ function header_link($SortKey, $DefaultWay = 'desc') { $Torrents = $GroupInfo['Torrents']; $GroupTime = $MaxSize = $TotalLeechers = $TotalSeeders = $TotalSnatched = 0; foreach ($Torrents as $T) { - if (isset($TorrentIDs[$T['ID']])) { - $GroupTime = max($GroupTime, strtotime($T['Time'])); - $MaxSize = max($MaxSize, $T['Size']); - $TotalLeechers += $T['Leechers']; - $TotalSeeders += $T['Seeders']; - $TotalSnatched += $T['Snatched']; - } + $GroupTime = max($GroupTime, strtotime($T['Time'])); + $MaxSize = max($MaxSize, $T['Size']); + $TotalLeechers += $T['Leechers']; + $TotalSeeders += $T['Seeders']; + $TotalSnatched += $T['Snatched']; } } else { - $Torrents = array($Result['id'] => $GroupInfo['Torrents'][$Result['id']]); + $TorrentID = $Key; + $Torrents = array($TorrentID => $GroupInfo['Torrents'][$TorrentID]); } $TorrentTags = new Tags($GroupInfo['TagList']); @@ -1021,11 +606,6 @@ function header_link($SortKey, $DefaultWay = 'desc') { foreach ($Torrents as $TorrentID => $Data) { // All of the individual torrents in the group - // If they're using the advanced search and have chosen enabled grouping, we just skip the torrents that don't check out - if (!isset($TorrentIDs[$TorrentID])) { - continue; - } - //Get report info for each torrent, use the cache if available, if not, add to it. $Reported = false; $Reports = Torrents::get_reports($TorrentID); diff --git a/sections/torrents/download.php b/sections/torrents/download.php index 77923338..225537f2 100644 --- a/sections/torrents/download.php +++ b/sections/torrents/download.php @@ -37,7 +37,7 @@ /* uTorrent Remote and various scripts redownload .torrent files periodically. To prevent this retardation from blowing bandwidth etc., let's block it if the .torrent file has been downloaded four times before */ -$ScriptUAs = array('BTWebClient*', 'Python-urllib*', 'python-requests*'); +$ScriptUAs = array('BTWebClient*', 'Python-urllib*', 'python-requests*', 'uTorrent*'); if (Misc::in_array_partial($_SERVER['HTTP_USER_AGENT'], $ScriptUAs)) { $DB->query(" SELECT 1 diff --git a/sections/user/takemoderate.php b/sections/user/takemoderate.php index 1078c7ab..425298b7 100644 --- a/sections/user/takemoderate.php +++ b/sections/user/takemoderate.php @@ -693,7 +693,7 @@ Once you are connected to our server you will need to join our disabled users channel. Type: /join '.BOT_DISABLED_CHAN.' -Please visit us soon so we can help you resolve this matter.'); +Please visit us soon so we can help you resolve this matter.', 'noreply'); } if ($MergeStatsFrom && check_perms('users_edit_ratio')) { diff --git a/sections/userhistory/ip_history.php b/sections/userhistory/ip_history.php index b2688330..20d53d58 100644 --- a/sections/userhistory/ip_history.php +++ b/sections/userhistory/ip_history.php @@ -16,35 +16,24 @@ if (!is_number($UserID)) { error(404); } - -$DB->query(" - SELECT - um.Username, - p.Level AS Class - FROM users_main AS um - LEFT JOIN permissions AS p ON p.ID = um.PermissionID - WHERE um.ID = $UserID"); -list($Username, $Class) = $DB->next_record(); - -if (!check_perms('users_view_ips', $Class)) { +$UserInfo = Users::user_info($UserID); +if (!check_perms('users_view_ips', $UserInfo['Class'])) { error(403); } -$UsersOnly = $_GET['usersonly']; +$UsersOnly = !empty($_GET['usersonly']); -if (isset($_POST['ip'])) { - $SearchIP = db_string(str_replace("*", "%", trim($_POST['ip']))); - $SearchIPQuery = " AND h1.IP LIKE '$SearchIP' "; +if (!empty($_GET['ip']) && trim($_GET['ip']) != '') { + $SearchIP = db_string(str_replace("*", "%", trim($_GET['ip']))); + $SearchIPQuery = "AND IP LIKE '$SearchIP'"; +} else { + $SearchIPQuery = ""; } -View::show_header("IP address history for $Username"); +View::show_header("IP address history for $UserInfo[Username]"); ?> <script type="text/javascript">//<![CDATA[ -function ShowIPs(rowname) { - $('tr[name="' + rowname + '"]').gtoggle(); - -} -function Ban(ip, id, elemID) { +function Ban(ip, elemID) { var notes = prompt("Enter notes for this ban"); if (notes != null && notes.length > 0) { var xmlhttp; @@ -74,7 +63,7 @@ function UnBan(ip, id, elemID) { xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { document.getElementById(elemID).innerHTML = "Ban"; - document.getElementById(elemID).onclick = function() { Ban(ip, id, elemID); return false; }; + document.getElementById(elemID).onclick = function() { Ban(ip, elemID); return false; }; } } xmlhttp.open("GET","tools.php?action=quick_ban&perform=delete&id=" + id + "&ip=" + ip, true); @@ -86,67 +75,107 @@ function UnBan(ip, id, elemID) { <? list($Page, $Limit) = Format::page_limit(IPS_PER_PAGE); -if ($UsersOnly == 1) { - $RS = $DB->query(" - SELECT - SQL_CALC_FOUND_ROWS - h1.IP, - h1.StartTime, - h1.EndTime, - GROUP_CONCAT(h2.UserID SEPARATOR '|'), - GROUP_CONCAT(h2.StartTime SEPARATOR '|'), - GROUP_CONCAT(IFNULL(h2.EndTime,0) SEPARATOR '|'), - GROUP_CONCAT(um2.Username SEPARATOR '|'), - GROUP_CONCAT(um2.Enabled SEPARATOR '|'), - GROUP_CONCAT(ui2.Donor SEPARATOR '|'), - GROUP_CONCAT(ui2.Warned SEPARATOR '|') - FROM users_history_ips AS h1 - LEFT JOIN users_history_ips AS h2 ON h2.IP = h1.IP AND h2.UserID != $UserID - LEFT JOIN users_main AS um2 ON um2.ID = h2.UserID - LEFT JOIN users_info AS ui2 ON ui2.UserID = h2.UserID - WHERE h1.UserID = '$UserID' - AND h2.UserID > 0 $SearchIPQuery - GROUP BY h1.IP, h1.StartTime - ORDER BY h1.StartTime DESC - LIMIT $Limit"); +if ($UsersOnly) { + $DB->query(" + SELECT DISTINCT IP + FROM users_history_ips + WHERE UserID = '$UserID' + $SearchIPQuery"); + + if ($DB->has_results()) { + $UserIPs = db_array($DB->collect('IP'), array(), true); + $DB->query(" + SELECT DISTINCT IP + FROM users_history_ips + WHERE UserID != '$UserID' + AND IP IN (" . implode(',', $UserIPs) . ")"); + unset($UserIPs); + + if ($DB->has_results()) { + $OtherIPs = db_array($DB->collect('IP'), array(), true); + $QueryID = $DB->query(" + SELECT + SQL_CALC_FOUND_ROWS + IP, + StartTime, + EndTime + FROM users_history_ips + WHERE UserID = '$UserID' + AND IP IN (" . implode(',', $OtherIPs) . ") + ORDER BY StartTime DESC + LIMIT $Limit"); + unset($OtherIPs); + } + } } else { - $RS = $DB->query(" + $QueryID = $DB->query(" SELECT SQL_CALC_FOUND_ROWS - h1.IP, - h1.StartTime, - h1.EndTime, - GROUP_CONCAT(h2.UserID SEPARATOR '|'), - GROUP_CONCAT(h2.StartTime SEPARATOR '|'), - GROUP_CONCAT(IFNULL(h2.EndTime,0) SEPARATOR '|'), - GROUP_CONCAT(um2.Username SEPARATOR '|'), - GROUP_CONCAT(um2.Enabled SEPARATOR '|'), - GROUP_CONCAT(ui2.Donor SEPARATOR '|'), - GROUP_CONCAT(ui2.Warned SEPARATOR '|') - FROM users_history_ips AS h1 - LEFT JOIN users_history_ips AS h2 ON h2.IP = h1.IP AND h2.UserID != $UserID - LEFT JOIN users_main AS um2 ON um2.ID = h2.UserID - LEFT JOIN users_info AS ui2 ON ui2.UserID = h2.UserID - WHERE h1.UserID = '$UserID' $SearchIPQuery - GROUP BY h1.IP, h1.StartTime - ORDER BY h1.StartTime DESC + IP, + StartTime, + EndTime + FROM users_history_ips + WHERE UserID = '$UserID' + $SearchIPQuery + ORDER BY StartTime DESC LIMIT $Limit"); } -$DB->query('SELECT FOUND_ROWS()'); -list($NumResults) = $DB->next_record(); -$DB->set_query_id($RS); + +if (isset($QueryID)) { + $DB->query('SELECT FOUND_ROWS()'); + list($NumResults) = $DB->next_record(); + $DB->set_query_id($QueryID); + $Results = $DB->to_array(false, MYSQLI_ASSOC); + $IPMatches = $IPMatchesUser = $IPMatchesIgnored = array(); +} else { + $NumResults = 0; + $Results = array(); +} + +if (!empty($Results)) { + $IPs = db_array($DB->collect('IP'), array(), true); + $DB->query(" + SELECT + UserID, + IP, + StartTime, + EndTime + FROM users_history_ips + WHERE IP IN (" . implode(',', $IPs) . ") + AND UserID != '$UserID' + AND UserID != 0 + ORDER BY StartTime DESC"); + unset($IPs); + + while ($Match = $DB->next_record(MYSQLI_ASSOC)) { + $OtherIP = $Match['IP']; + $OtherUserID = $Match['UserID']; + if (!isset($IPMatchesUser[$OtherIP][$OtherUserID])) { + $IPMatchesUser[$OtherIP][$OtherUserID] = 0; + } + if ($IPMatchesUser[$OtherIP][$OtherUserID] < 500) { + $IPMatches[$OtherIP][] = $Match; + } else { + if (!isset($IPMatchesIgnored[$OtherIP][$OtherUserID])) { + $IPMatchesIgnored[$OtherIP][$OtherUserID] = 0; + } + $IPMatchesIgnored[$OtherIP][$OtherUserID]++; + } + $IPMatchesUser[$OtherIP][$OtherUserID]++; + } +} $Pages = Format::get_pages($Page, $NumResults, IPS_PER_PAGE, 9); ?> <div class="thin"> <div class="header"> - <h2>IP address history for <a href="user.php?id=<?=$UserID?>"><?=$Username?></a></h2> + <h2>IP address history for <a href="user.php?id=<?=$UserID?>"><?=$UserInfo['Username']?></a></h2> <div class="linkbox"> <? if ($UsersOnly) { ?> - <a href="userhistory.php?action=ips&userid=<?=$UserID?>" class="brackets">View all IP addresses</a> + <a href="userhistory.php?<?=Format::get_url(array('usersonly'))?>" class="brackets">View all IP addresses</a> <? } else { ?> - <a href="userhistory.php?action=ips&userid=<?=$UserID?>&usersonly=1" class="brackets">View IP addresses with users</a> + <a href="userhistory.php?<?=Format::get_url()?>&usersonly=1" class="brackets">View IP addresses with users</a> <? } ?> </div> <? if ($Pages) { ?> @@ -159,8 +188,13 @@ function UnBan(ip, id, elemID) { </tr> <tr><td> - <form class="search_form" name="ip_log" method="post" action=""> - <input type="text" name="ip" /> + <form class="search_form" name="ip_log" method="get" action=""> + <input type="hidden" name="action" value="<?=$_GET['action']?>" /> + <input type="hidden" name="userid" value="<?=$UserID?>" /> +<? if ($UsersOnly) { ?> + <input type="hidden" name="usersonly" value="1" /> +<? } ?> + <input type="text" name="ip" value="<?=Format::form('ip')?>" /> <input type="submit" value="Search" /> Wildcard (*) search examples: 127.0.* or 1*2.0.*.1 or *.*.*.* </form> @@ -170,91 +204,110 @@ function UnBan(ip, id, elemID) { <table id="iphistory"> <tr class="colhead"> <td>IP address</td> - <td>Started <a href="#" onclick="$('#iphistory td:nth-child(2), #iphistory td:nth-child(4)').ghide(); $('#iphistory td:nth-child(3), #iphistory td:nth-child(5)').gshow(); return false;" class="brackets">Toggle</a></td> - <td class="hidden">Started <a href="#" onclick="$('#iphistory td:nth-child(2), #iphistory td:nth-child(4)').gshow(); $('#iphistory td:nth-child(3), #iphistory td:nth-child(5)').ghide(); return false;" class="brackets">Toggle</a></td> + <td>Started <a href="#" onclick="$('#iphistory .reltime').gtoggle(); $('#iphistory .abstime').gtoggle(); return false;" class="brackets">Toggle</a></td> <td>Ended</td> <td class="hidden">Ended</td> <td>Elapsed</td> </tr> <? -$counter = 0; -$IPs = array(); -$Results = $DB->to_array(); +$Counter = 0; +$IPBanChecks = array(); +$PrintedIPs = array(); $CanManageIPBans = check_perms('admin_manage_ipbans'); - foreach ($Results as $Index => $Result) { - list($IP, $StartTime, $EndTime, $UserIDs, $UserStartTimes, $UserEndTimes, $Usernames, $UsersEnabled, $UsersDonor, $UsersWarned) = $Result; - - $HasDupe = false; - $UserIDs = explode('|', $UserIDs); - if (!$EndTime) { + $IP = $Result['IP']; + $StartTime = $Result['StartTime']; + $EndTime = $Result['EndTime']; + if (!$Result['EndTime']) { $EndTime = sqltime(); } - if ($UserIDs[0] != 0) { - $HasDupe = true; - $UserStartTimes = explode('|', $UserStartTimes); - $UserEndTimes = explode('|', $UserEndTimes); - $Usernames = explode('|', $Usernames); - $UsersEnabled = explode('|', $UsersEnabled); - $UsersDonor = explode('|', $UsersDonor); - $UsersWarned = explode('|', $UsersWarned); + $OtherUsers = isset($IPMatches[$IP]) ? $IPMatches[$IP] : array(); + $ElementID = 'ip_' . strtr($IP, '.', '-'); + $FirstOccurrence = !isset($IPIndexes[$IP]); + if ($FirstOccurrence) { + $IPIndexes[$IP] = $Index; } ?> - <tr class="rowa"> + <tr class="rowa" <?=$FirstOccurrence ? "id=\"$ElementID\"" : ''?>> <td> <?=$IP?> (<?=Tools::get_country_code_by_ajax($IP)?>)<? if ($CanManageIPBans) { - if (!isset($IPs[$IP])) { - $sql = " - SELECT ID, FromIP, ToIP - FROM ip_bans - WHERE '".Tools::ip_to_unsigned($IP)."' BETWEEN FromIP AND ToIP - LIMIT 1"; - $DB->query($sql); - - if ($DB->has_results()) { - $IPs[$IP] = true; + if (!isset($IPBanChecks[$IP])) { + if (Tools::site_ban_ip($IP)) { + $IPBanChecks[$IP] = true; ?> <strong>[Banned]</strong> <? } else { - $IPs[$IP] = false; + $IPBanChecks[$IP] = false; ?> - <a id="<?=$counter?>" href="#" onclick="Ban('<?=$IP?>', '<?=$ID?>', '<?=$counter?>'); this.onclick = null; return false;" class="brackets">Ban</a> + <a id="<?=$Counter?>" href="#" onclick="Ban('<?=$IP?>', '<?=$Counter?>'); this.onclick = null; return false;" class="brackets">Ban</a> <? } - $counter++; + $Counter++; } } ?> <br /> <?=Tools::get_host_by_ajax($IP)?> - <?=($HasDupe ? '<a href="#" onclick="ShowIPs('.$Index.'); return false;">('.count($UserIDs).')</a>' : '(0)')?> +<? + if (!empty($OtherUsers)) { + if ($FirstOccurrence || count($OtherUsers) <= 100) { +?> + <a href="#" onclick="$('.otherusers' + <?=$Index?>).gtoggle(); return false;">(<?=count($OtherUsers)?>)</a> +<? + } else { +?> + <a href="#<?=$ElementID?>" onclick="$('.otherusers' + <?=$IPIndexes[$IP]?>).gshow();">(<?=count($OtherUsers)?>)</a> +<? + } + } else { +?> + (0) +<? + } +?> + </td> + <td> + <span class="reltime"><?=time_diff($StartTime)?></span> + <span class="abstime hidden"><?=$StartTime?></span> + </td> + <td> + <span class="reltime"><?=time_diff($EndTime)?></span> + <span class="abstime hidden"><?=$EndTime?></span> </td> - <td><?=time_diff($StartTime)?></td> - <td class="hidden"><?=$StartTime?></td> - <td><?=time_diff($EndTime)?></td> - <td class="hidden"><?=$EndTime?></td> <td><?//time_diff(strtotime($StartTime), strtotime($EndTime)); ?></td> </tr> <? - if ($HasDupe) { - $HideMe = (count($UserIDs) > 10); - foreach ($UserIDs as $Key => $Val) { - if (!$UserEndTimes[$Key]) { - $UserEndTimes[$Key] = sqltime(); + if (!empty($OtherUsers) && ($FirstOccurrence || count($OtherUsers) < 100)) { + $HideMe = (count($OtherUsers) > 10); + foreach ($OtherUsers as $OtherUser) { + if (!$OtherUser['EndTime']) { + $OtherUser['EndTime'] = sqltime(); } ?> - <tr class="rowb<?=($HideMe ? ' hidden' : '')?>" name="<?=$Index?>"> - <td>  » <?=Users::format_username($Val, true, true, true)?></td> - <td><?=time_diff($UserStartTimes[$Key])?></td> - <td class="hidden"><?=$UserStartTimes[$Key]?></td> - <td><?=time_diff($UserEndTimes[$Key])?></td> - <td class="hidden"><?=$UserEndTimes[$Key]?></td> - <td><?//time_diff(strtotime($UserStartTimes[$Key]), strtotime($UserEndTimes[$Key])); ?></td> + <tr class="rowb otherusers<?=$Index?><?=($HideMe ? ' hidden' : '')?>"> + <td>  » <?=Users::format_username($OtherUser['UserID'], true, true, true)?></td> + <td> + <span class="reltime"><?=time_diff($OtherUser['StartTime'])?></span> + <span class="hidden abstime"><?=$OtherUser['StartTime']?></span> + </td> + <td> + <span class="reltime"><?=time_diff($OtherUser['EndTime'])?></span> + <span class="hidden abstime"><?=$OtherUser['EndTime']?></span> + </td> + <td><?//time_diff(strtotime($OtherUser['StartTime']), strtotime($OtherUser['EndTime'])); ?></td> </tr> <? - + } + if (isset($IPMatchesIgnored[$IP])) { + foreach ($IPMatchesIgnored[$IP] as $OtherUserID => $MatchCount) { +?> + <tr class="rowb otherusers<?=$Index?><?=($HideMe ? ' hidden' : '')?>"> + <td colspan="4">  » <?=$MatchCount?> matches skipped for <?=Users::format_username($OtherUserID, false, false, false)?></td> + </tr> +<? + } } } }