diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/html/home.php | 19 | ||||
-rw-r--r-- | web/html/index.php | 14 | ||||
-rw-r--r-- | web/html/js/bootstrap-typeahead.js | 311 | ||||
-rw-r--r-- | web/html/packages.php | 15 | ||||
-rw-r--r-- | web/lib/aur.inc.php | 2 | ||||
-rw-r--r-- | web/lib/aurjson.class.php | 22 | ||||
-rw-r--r-- | web/lib/pkgfuncs.inc.php | 155 | ||||
-rw-r--r-- | web/template/header.php | 3 |
8 files changed, 465 insertions, 76 deletions
diff --git a/web/html/home.php b/web/html/home.php index 4e489ba..0b51d55 100644 --- a/web/html/home.php +++ b/web/html/home.php @@ -95,7 +95,7 @@ $dbh = db_connect(); <fieldset> <label for="pkgsearch-field"><?= __('Package Search') ?>:</label> <input type="hidden" name="O" value="0" /> - <input type="text" name="K" size="30" value="<?php if (isset($_REQUEST["K"])) { print stripslashes(trim(htmlspecialchars($_REQUEST["K"], ENT_QUOTES))); } ?>" maxlength="35" /> + <input id="pkgsearch-field" type="text" name="K" size="30" value="<?php if (isset($_REQUEST["K"])) { print stripslashes(trim(htmlspecialchars($_REQUEST["K"], ENT_QUOTES))); } ?>" maxlength="35" /> </fieldset> </form> </div> @@ -107,5 +107,22 @@ $dbh = db_connect(); </div> </div> +<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script> +<script type="text/javascript" src="/js/bootstrap-typeahead.js"></script> +<script type="text/javascript"> +$(document).ready(function() { + $('#pkgsearch-field').typeahead({ + source: function(query, callback) { + $.getJSON('<?= get_uri('/rpc'); ?>', {type: "suggest", arg: query}, function(data) { + callback(data); + }); + }, + matcher: function(item) { return true; }, + sorter: function(items) { return items; }, + menu: '<ul class="pkgsearch-typeahead"></ul>', + items: 20 + }).attr('autocomplete', 'off'); +}); +</script> <?php html_footer(AUR_VERSION); diff --git a/web/html/index.php b/web/html/index.php index 3b46ab9..c51f409 100644 --- a/web/html/index.php +++ b/web/html/index.php @@ -22,12 +22,6 @@ if (!empty($tokens[1]) && '/' . $tokens[1] == get_pkg_route()) { } if (!empty($tokens[3])) { - if ($tokens[3] == 'voters') { - $_GET['ID'] = pkgid_from_name($tokens[2]); - include('voters.php'); - return; - } - /* TODO: Remove support for legacy URIs and move these * actions to separate modules. */ switch ($tokens[3]) { @@ -55,6 +49,10 @@ if (!empty($tokens[1]) && '/' . $tokens[1] == get_pkg_route()) { case "merge": include('pkgmerge.php'); return; + case "voters": + $_GET['ID'] = pkgid_from_name($tokens[2]); + include('voters.php'); + return; default: header("HTTP/1.0 404 Not Found"); include "./404.php"; @@ -120,6 +118,10 @@ if (!empty($tokens[1]) && '/' . $tokens[1] == get_pkg_route()) { header("Content-Type: image/png"); include "./$path"; break; + case "/js/bootstrap-typeahead.js": + header("Content-Type: application/javascript"); + include "./$path"; + break; default: header("HTTP/1.0 404 Not Found"); include "./404.php"; diff --git a/web/html/js/bootstrap-typeahead.js b/web/html/js/bootstrap-typeahead.js new file mode 100644 index 0000000..4f333e8 --- /dev/null +++ b/web/html/js/bootstrap-typeahead.js @@ -0,0 +1,311 @@ +/* ============================================================= + * bootstrap-typeahead.js v2.2.1 + * http://twitter.github.com/bootstrap/javascript.html#typeahead + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function($){ + + "use strict"; // jshint ;_; + + + /* TYPEAHEAD PUBLIC CLASS DEFINITION + * ================================= */ + + var Typeahead = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.typeahead.defaults, options) + this.matcher = this.options.matcher || this.matcher + this.sorter = this.options.sorter || this.sorter + this.highlighter = this.options.highlighter || this.highlighter + this.updater = this.options.updater || this.updater + this.$menu = $(this.options.menu).appendTo('body') + this.source = this.options.source + this.shown = false + this.listen() + } + + Typeahead.prototype = { + + constructor: Typeahead + + , select: function () { + var val = this.$menu.find('.active').attr('data-value') + this.$element + .val(this.updater(val)) + .change() + return this.hide() + } + + , updater: function (item) { + return item + } + + , show: function () { + var pos = $.extend({}, this.$element.offset(), { + height: this.$element[0].offsetHeight + }) + + this.$menu.css({ + top: pos.top + pos.height + , left: pos.left + }) + + this.$menu.show() + this.shown = true + return this + } + + , hide: function () { + this.$menu.hide() + this.shown = false + return this + } + + , lookup: function (event) { + var items + + this.query = this.$element.val() + + if (!this.query || this.query.length < this.options.minLength) { + return this.shown ? this.hide() : this + } + + items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source + + return items ? this.process(items) : this + } + + , process: function (items) { + var that = this + + items = $.grep(items, function (item) { + return that.matcher(item) + }) + + items = this.sorter(items) + + if (!items.length) { + return this.shown ? this.hide() : this + } + + return this.render(items.slice(0, this.options.items)).show() + } + + , matcher: function (item) { + return ~item.toLowerCase().indexOf(this.query.toLowerCase()) + } + + , sorter: function (items) { + var beginswith = [] + , caseSensitive = [] + , caseInsensitive = [] + , item + + while (item = items.shift()) { + if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) + else if (~item.indexOf(this.query)) caseSensitive.push(item) + else caseInsensitive.push(item) + } + + return beginswith.concat(caseSensitive, caseInsensitive) + } + + , highlighter: function (item) { + var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') + return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { + return '<strong>' + match + '</strong>' + }) + } + + , render: function (items) { + var that = this + + items = $(items).map(function (i, item) { + i = $(that.options.item).attr('data-value', item) + i.find('a').html(that.highlighter(item)) + return i[0] + }) + + items.first().addClass('active') + this.$menu.html(items) + return this + } + + , next: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , next = active.next() + + if (!next.length) { + next = $(this.$menu.find('li')[0]) + } + + next.addClass('active') + } + + , prev: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , prev = active.prev() + + if (!prev.length) { + prev = this.$menu.find('li').last() + } + + prev.addClass('active') + } + + , listen: function () { + this.$element + .on('blur', $.proxy(this.blur, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + if (this.eventSupported('keydown')) { + this.$element.on('keydown', $.proxy(this.keydown, this)) + } + + this.$menu + .on('click', $.proxy(this.click, this)) + .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) + } + + , eventSupported: function(eventName) { + var isSupported = eventName in this.$element + if (!isSupported) { + this.$element.setAttribute(eventName, 'return;') + isSupported = typeof this.$element[eventName] === 'function' + } + return isSupported + } + + , move: function (e) { + if (!this.shown) return + + switch(e.keyCode) { + case 9: // tab + case 13: // enter + case 27: // escape + e.preventDefault() + break + + case 38: // up arrow + e.preventDefault() + this.prev() + break + + case 40: // down arrow + e.preventDefault() + this.next() + break + } + + e.stopPropagation() + } + + , keydown: function (e) { + this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27]) + this.move(e) + } + + , keypress: function (e) { + if (this.suppressKeyPressRepeat) return + this.move(e) + } + + , keyup: function (e) { + switch(e.keyCode) { + case 40: // down arrow + case 38: // up arrow + case 16: // shift + case 17: // ctrl + case 18: // alt + break + + case 9: // tab + case 13: // enter + if (!this.shown) return + this.select() + break + + case 27: // escape + if (!this.shown) return + this.hide() + break + + default: + this.lookup() + } + + e.stopPropagation() + e.preventDefault() + } + + , blur: function (e) { + var that = this + setTimeout(function () { that.hide() }, 150) + } + + , click: function (e) { + e.stopPropagation() + e.preventDefault() + this.select() + this.$element.focus() + } + + , mouseenter: function (e) { + this.$menu.find('.active').removeClass('active') + $(e.currentTarget).addClass('active') + } + + } + + + /* TYPEAHEAD PLUGIN DEFINITION + * =========================== */ + + $.fn.typeahead = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('typeahead') + , options = typeof option == 'object' && option + if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.typeahead.defaults = { + source: [] + , items: 8 + , menu: '<ul class="typeahead dropdown-menu"></ul>' + , item: '<li><a href="#"></a></li>' + , minLength: 1 + } + + $.fn.typeahead.Constructor = Typeahead + + + /* TYPEAHEAD DATA-API + * ================== */ + + $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { + var $this = $(this) + if ($this.data('typeahead')) return + e.preventDefault() + $this.typeahead($this.data()) + }) + +}(window.jQuery); diff --git a/web/html/packages.php b/web/html/packages.php index 094c221..c1e54e1 100644 --- a/web/html/packages.php +++ b/web/html/packages.php @@ -20,9 +20,12 @@ if (!isset($pkgid) || !isset($pkgname)) { } } -# Set the title to the current query if required +# Set the title to the current query and get package details if required +$details = array(); + if (isset($pkgname)) { $title = $pkgname; + $details = get_package_details($pkgid); } else if (!empty($_GET['K'])) { $title = __("Search Criteria") . ": " . $_GET['K']; } else { @@ -51,9 +54,9 @@ if (isset($_POST['IDs'])) { $output = ""; if (check_token()) { if (current_action("do_Flag")) { - $output = pkg_flag($atype, $ids, true); + $output = pkg_flag($atype, $ids); } elseif (current_action("do_UnFlag")) { - $output = pkg_flag($atype, $ids, False); + $output = pkg_unflag($atype, $ids); } elseif (current_action("do_Adopt")) { $output = pkg_adopt($atype, $ids, true); } elseif (current_action("do_Disown")) { @@ -93,7 +96,7 @@ if (check_token()) { } } -html_header($title); +html_header($title, $details); ?> <?php if ($output): ?> @@ -105,10 +108,10 @@ if (isset($pkgid)) { include('pkg_search_form.php'); if ($pkgid) { if (isset($_COOKIE["AURSID"])) { - package_details($pkgid, $_COOKIE["AURSID"]); + display_package_details($pkgid, $details, $_COOKIE["AURSID"]); } else { - package_details($pkgid, null); + display_package_details($pkgid, $details, null); } } else { print __("Error trying to retrieve package details.")."<br />\n"; diff --git a/web/lib/aur.inc.php b/web/lib/aur.inc.php index e02c835..d8c5cb4 100644 --- a/web/lib/aur.inc.php +++ b/web/lib/aur.inc.php @@ -297,7 +297,7 @@ function db_connect() { * * @return void */ -function html_header($title="") { +function html_header($title="", $details=array()) { global $AUR_LOCATION; global $DISABLE_HTTP_LOGIN; global $LANG; diff --git a/web/lib/aurjson.class.php b/web/lib/aurjson.class.php index 949c34f..616b783 100644 --- a/web/lib/aurjson.class.php +++ b/web/lib/aurjson.class.php @@ -15,7 +15,7 @@ include_once("aur.inc.php"); class AurJSON { private $dbh = false; private static $exposed_methods = array( - 'search', 'info', 'multiinfo', 'msearch' + 'search', 'info', 'multiinfo', 'msearch', 'suggest' ); private static $fields = array( 'Packages.ID', 'Name', 'Version', 'CategoryID', 'Description', 'URL', @@ -276,5 +276,25 @@ class AurJSON { return $this->process_query('msearch', $where_condition); } + + /** + * Get all package names that start with $search. + * @param string $search Search string. + * @return string The JSON formatted response data. + **/ + private function suggest($search) { + $query = 'SELECT Name FROM Packages WHERE Name LIKE ' . + $this->dbh->quote(addcslashes($search, '%_') . '%') . + ' ORDER BY Name ASC LIMIT 20'; + + $result = $this->dbh->query($query); + $result_array = array(); + + if ($result) { + $result_array = $result->fetchAll(PDO::FETCH_COLUMN, 0); + } + + return json_encode($result_array); + } } diff --git a/web/lib/pkgfuncs.inc.php b/web/lib/pkgfuncs.inc.php index cfdd9a7..568ca3d 100644 --- a/web/lib/pkgfuncs.inc.php +++ b/web/lib/pkgfuncs.inc.php @@ -432,17 +432,52 @@ function pkgname_is_blacklisted($name, $dbh=NULL) { } /** + * Get the package details + * + * @param string $id The package ID to get description for + * @param \PDO $dbh An already established database connection + * + * @return array The package's details OR error message + **/ +function get_package_details($id=0, $dbh=NULL) { + if(!$dbh) { + $dbh = db_connect(); + } + + $q = "SELECT Packages.*,Category "; + $q.= "FROM Packages,PackageCategories "; + $q.= "WHERE Packages.CategoryID = PackageCategories.ID "; + $q.= "AND Packages.ID = " . intval($id); + $result = $dbh->query($q); + + $row = array(); + + if (!$result) { + $row['error'] = __("Error retrieving package details."); + } + else { + $row = $result->fetch(PDO::FETCH_ASSOC); + if (empty($row)) { + $row['error'] = __("Package details could not be found."); + } + } + + return $row; +} + +/** * Display the package details page * * @global string $AUR_LOCATION The AUR's URL used for notification e-mails * @global bool $USE_VIRTUAL_URLS True if using URL rewriting, otherwise false * @param string $id The package ID to get details page for + * @param array $row Package details retrieved by get_package_details * @param string $SID The session ID of the visitor * @param \PDO $dbh An already established database connection * * @return void */ -function package_details($id=0, $SID="", $dbh=NULL) { +function display_package_details($id=0, $row, $SID="", $dbh=NULL) { global $AUR_LOCATION; global $USE_VIRTUAL_URLS; @@ -450,42 +485,28 @@ function package_details($id=0, $SID="", $dbh=NULL) { $dbh = db_connect(); } - $q = "SELECT Packages.*,Category "; - $q.= "FROM Packages,PackageCategories "; - $q.= "WHERE Packages.CategoryID = PackageCategories.ID "; - $q.= "AND Packages.ID = " . intval($id); - $result = $dbh->query($q); - - if (!$result) { - print "<p>" . __("Error retrieving package details.") . "</p>\n"; + if (isset($row['error'])) { + print "<p>" . $row['error'] . "</p>\n"; } else { - $row = $result->fetch(PDO::FETCH_ASSOC); - if (empty($row)) { - print "<p>" . __("Package details could not be found.") . "</p>\n"; + include('pkg_details.php'); - } - else { - include('pkg_details.php'); - - # Actions Bar - if ($SID) { - include('actions_form.php'); - if (isset($_REQUEST['comment']) && check_token()) { - $uid = uid_from_sid($SID, $dbh); - add_package_comment($id, $uid, $_REQUEST['comment'], $dbh); - } - include('pkg_comment_form.php'); + # Actions Bar + if ($SID) { + include('actions_form.php'); + if (isset($_REQUEST['comment']) && check_token()) { + $uid = uid_from_sid($SID, $dbh); + add_package_comment($id, $uid, $_REQUEST['comment'], $dbh); } + include('pkg_comment_form.php'); + } - # Print Comments - $comments = package_comments($id, $dbh); - if (!empty($comments)) { - include('pkg_comments.php'); - } + # Print Comments + $comments = package_comments($id, $dbh); + if (!empty($comments)) { + include('pkg_comments.php'); } } - return; } @@ -772,33 +793,24 @@ function sanitize_ids($ids) { } /** - * Flag and un-flag packages out-of-date + * Flag package(s) as out-of-date * * @global string $AUR_LOCATION The AUR's URL used for notification e-mails * @param string $atype Account type, output of account_from_sid * @param array $ids Array of package IDs to flag/unflag - * @param bool $action true flags out-of-date, false un-flags. Flags by default * * @return string Translated success or error messages */ -function pkg_flag ($atype, $ids, $action=true, $dbh=NULL) { +function pkg_flag($atype, $ids, $dbh=NULL) { global $AUR_LOCATION; if (!$atype) { - if ($action) { - return __("You must be logged in before you can flag packages."); - } else { - return __("You must be logged in before you can unflag packages."); - } + return __("You must be logged in before you can flag packages."); } $ids = sanitize_ids($ids); if (empty($ids)) { - if ($action) { - return __("You did not select any packages to flag."); - } else { - return __("You did not select any packages to unflag."); - } + return __("You did not select any packages to flag."); } if(!$dbh) { @@ -806,25 +818,13 @@ function pkg_flag ($atype, $ids, $action=true, $dbh=NULL) { } $q = "UPDATE Packages SET"; - if ($action) { - $q.= " OutOfDateTS = UNIX_TIMESTAMP()"; - } - else { - $q.= " OutOfDateTS = NULL"; - } + $q.= " OutOfDateTS = UNIX_TIMESTAMP()"; $q.= " WHERE ID IN (" . implode(",", $ids) . ")"; - - if (!$action && ($atype != "Trusted User" && $atype != "Developer")) { - $q.= "AND MaintainerUID = " . uid_from_sid($_COOKIE["AURSID"], $dbh); - } - - if ($action) { - $q.= " AND OutOfDateTS IS NULL"; - } + $q.= " AND OutOfDateTS IS NULL"; $affected_pkgs = $dbh->exec($q); - if ($action && $affected_pkgs > 0) { + if ($affected_pkgs > 0) { # Notify of flagging by email $f_name = username_from_sid($_COOKIE['AURSID'], $dbh); $f_email = email_from_sid($_COOKIE['AURSID'], $dbh); @@ -846,9 +846,42 @@ function pkg_flag ($atype, $ids, $action=true, $dbh=NULL) { } } - if ($action) { - return __("The selected packages have been flagged out-of-date."); - } else { + return __("The selected packages have been flagged out-of-date."); +} + +/** + * Unflag package(s) as out-of-date + * + * @param string $atype Account type, output of account_from_sid + * @param array $ids Array of package IDs to flag/unflag + * + * @return string Translated success or error messages + */ +function pkg_unflag($atype, $ids, $dbh=NULL) { + if (!$atype) { + return __("You must be logged in before you can unflag packages."); + } + + $ids = sanitize_ids($ids); + if (empty($ids)) { + return __("You did not select any packages to unflag."); + } + + if(!$dbh) { + $dbh = db_connect(); + } + + $q = "UPDATE Packages SET "; + $q.= "OutOfDateTS = NULL "; + $q.= "WHERE ID IN (" . implode(",", $ids) . ") "; + + if ($atype != "Trusted User" && $atype != "Developer") { + $q.= "AND MaintainerUID = " . uid_from_sid($_COOKIE["AURSID"], $dbh); + } + + $result = $dbh->exec($q); + + if ($result) { return __("The selected packages have been unflagged."); } } diff --git a/web/template/header.php b/web/template/header.php index 92cb2ff..9cefedc 100644 --- a/web/template/header.php +++ b/web/template/header.php @@ -10,6 +10,9 @@ <link rel='shortcut icon' href='/images/favicon.ico' /> <link rel='alternate' type='application/rss+xml' title='Newest Packages RSS' href='<?= get_uri('/rss/'); ?>' /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +<?php if (isset($details['Description']) && !empty($details['Description'])): ?> + <meta name="description" content="<?= htmlspecialchars($details['Description']) ?>" /> +<?php endif; ?> </head> <body> <div id="archnavbar" class="anb-aur"> |