Skip to content
Snippets Groups Projects
Forked from Stud.IP / Stud.IP
458 commits behind the upstream repository.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
QuickSearch.php 17.43 KiB
<?php
# Lifter010: TODO
/**
 * QuickSearch.php - GUI class for quciksearch
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * @author      Rasmus <fuhse@data-quest.de>
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
 * @category    Stud.IP
 */

/**
 * This class provides a small and intuitive GUI-element for an instant search of
 * courses, persons, institutes or other items. Mainly the structure to include
 * a QuickSearch-field is the following:
 *  //code-begin
 *  $sf = new QuickSearch("username");
 *    $sf->withButton();
 *  $sf->specialSQL("SELECT username, CONCAT(Vorname, \" \", Nachname) " .
 *     "FROM auth_user_md5 " .
 *     "WHERE CONCAT(Vorname, \" \", Nachname) LIKE :input " .
 *        "AND perms = 'dozent'", _("Dozenten suchen"));
 *  print $sf->render();
 *  //code-end
 * This code should be included into an html <form>-tag. It will provide a nice looking
 * input-field with a button (this is the $sf->withButton() command for), a javascript
 * instant-search for your items (in this case 'Dozenten') and also a non-javascript
 * oldschool version of a searchfield with a select-box in step 2 of the search (first
 * write what you search, click on the button and then select in the select-box what
 * you wanted to have).
 * You can handle the searchfield in your form-tag as if you have written an
 * '<input type="text" name="username">'.
 *
 * For most cases you may only want to search for persons, courses or institutes.
 * Thus a shortcut is implemented in this class. You may write
 *  //code-begin
 *  $sf = new QuickSearch("username", "username");
 *    $sf->withButton();
 *  print $sf->render();
 *  //code-end
 * to receive a searchfield that is automatically searching for users and inserts
 * the selected users's username in the searchfield. The first parameter of the
 * constructor 'new Quicksearch' is the name of the variable in your form and is
 * completely free to name. The (optional) second parameter describes what you are
 * searching for: username, user_id, Seminar_id or Institut_id.
 *
 * Also you can do method-chaining with this class, so you can press everything
 * you need infront of your semicolon. Watch this example:
 *  //code-begin
 *  print QuickSearch::get("username", "username")->withButton->render();
 *  //code-end
 *
 * Lastly you can replace the second argument of the constructor (or get-method)
 * by an object whose class extends the SearchType-class. This might be
 * useful to create your own searches and handle them with oop-style or even
 * use a totally different search-engine like lucene-index! All you need to
 * do so is implement your searchclass and follow this example:
 *  //code-begin
 *  class TeacherSearch extends SearchType {
 *    ...
 *  }
 *  $searcher = new TeacherSearch();
 *  print QuickSearch::get("username", $searcher)->withButton->render();
 *  //code-end
 * Watch the SearchType class in lib/classes/searchtypes/SearchType.php
 * for details.
 * Enjoy!
 */
class QuickSearch
{

    const GC_LIFETIME = 10800; // = 3 * 60 * 60 = 3 hours

    static $count_QS = 0;       //static counter of all instances of this class

    private $name;              //name of the input/select field
    private $search;            //may be an object or a string
    private $avatarLike;        //like "user_id", "username", "Seminar_id" or stuff
    private $withButton;        //if true, the field will be displayed with a looking-glass-button to click on
    private $selectBox = true;
    private $withAttributes = [];
    private $box_width = "233"; //width of the box withButton
    private $box_align = "right";//align of the lookingglass in the withButton-box
    private $autocomplete_disabled = false;
    private $search_button_name;
    private $reset_button_name;
    private $defaultID = null;
    private $defaultName = null;
    private $jsfunction = null;
    private $inputClass = null;
    private $inputStyle = null;
    private $specialQuery = null;
    private $minLength = 3;


    /**
     * Deletes all older requests that have not been used for three hours
     * from the session
     *
     * @return int Number of removed searches
     */
    public static function garbageCollect()
    {
        if (empty($_SESSION['QuickSearches'])) {
            return 0;
        }
        $count = count($_SESSION['QuickSearches']);

        $_SESSION['QuickSearches'] = array_filter($_SESSION['QuickSearches'], function ($query) {
            return $query['time'] + QuickSearch::GC_LIFETIME > time();
        });

        return $count - count($_SESSION['QuickSearches']);
    }

    /**
     * Retrieves the search object for the given id previously stored in
     * the session.
     *
     * @param String $query_id Id of the quicksearch object
     * @return SearchType Quicksearch object
     * @throws RuntimeException when the given query does not exist in session
     */
    public static function getFromSession($query_id)
    {
        self::garbageCollect();

        if (!isset($_SESSION['QuickSearches'][$query_id])) {
            throw new RuntimeException('Quicksearch id not in session');
        }

        // Store last access to search
        $_SESSION['QuickSearches'][$query_id]['time'] = time();

        $query = $_SESSION['QuickSearches'][$query_id];
        if ($query['includePath']) {
            include_once $query['includePath'];
        }

        return unserialize($query['object']);

    }

    /**
     * returns an instance of QuickSearch so you can use it as singleton
     *
     * @param string $name the name of the destinated variable in your html-form. Handle it
     * as if it was an '<input type="text" name="yourname">' input.
     * @param string $search if set to user_id, username, Seminar_id, Arbeitsgruppe_id or Institute_id
     * the searchfield will automatically search for persons, courses, workgroups, institutes and
     * you don't need to call the specialSearch-method.
     *
     * @return static
     */
    public static function get($name, $search = NULL)
    {
        return new static($name, $search);
    }


    /**
     * constructor which prepares a searchfield for persons, courses, institutes or
     * special items you may want to search for. This is a GUI-class, see
     * QuickSearch.php for further documentation.
     *
     * @param string $name the name of the destinated variable in your html-form. Handle it
     * as if it was an '<input type="text" name="yourname">' input.
     * @param string $search if set to user_id, username, Seminar_id, Arbeitsgruppe_id or Institute_id
     * the searchfield will automatically search for persons, courses, workgroups, institutes and
     * you don't need to call the specialSearch-method.
     *
     * @return void
     */
    final public function __construct($name, $search = NULL)
    {
        self::$count_QS++;
        $this->name = $name;
        $this->withButton = false;
        $this->avatarLike = "";
        if ($search instanceof SearchType) {
            $this->search = $search;
        } else {
            $this->search = NULL;
        }
        $this->setAttributes([]);
    }

    /**
     * if set to true, the searchfield will be a nice-looking grey searchfield with
     * a magnifier-symbol as a submit-button. Set this to false to create your own
     * submit-button and style of the form.
     * @param mixed $design  associative array of params.
     *
     * @return QuickSearch
     */
    public function withButton($design = [])
    {
        $this->withButton = true;
        if (isset($design['width'])) {
            $this->box_width = $design['width'];
        }
        $this->box_align = $design['align'] ?? "right";
        $this->search_button_name = $design['search_button_name'] ?? '';
        $this->reset_button_name = $design['reset_button_name'] ?? '';
        return $this;
    }

    /**
     * this will disable a submit button for the searchfield
     *
     * @return QuickSearch
     */
    public function withoutButton()
    {
        $this->withButton = false;
        return $this;
    }

    /**
     * Here you can set a default-value for the searchfield
     *
     * @param string $valueID the default-ID that should be stored
     * @param string $valueName the default value, that should be displayed
     * - remember that these may not be the same, they may be
     *   something like "ae2b1fca515949e5d54fb22b8ed95575", "test_dozent"
     *
     * @return QuickSearch
     */
    public function defaultValue($valueID, $valueName)
    {
        $this->defaultID = $valueID;
        $this->defaultName = $valueName;
        return $this;
    }

    /**
     * defines a css class for the searchfield
     *
     * @param string $class any css class name for the "input type=text" tag
     *
     * @return QuickSearch
     */
    public function setInputClass($class)
    {
        $this->withAttributes['class'] = $class;
        return $this;
    }

    /**
     * defines css-proporties for searchfield that will be included as 'style="$style"'
     *
     * @param string $style one or more css-proporties separated with ";"
     *
     * @return QuickSearch
     */
    public function setInputStyle($style)
    {
        $this->withAttributes['style'] = $style;
        return $this;
    }

    /**
     * Set the minimum length to start searching
     *
     * @param int $minLength
     *
     * @return QuickSearch
     */
    public function setMinLength(int $minLength)
    {
        $this->minLength = $minLength;

        return $this;
    }
    /**
     * disables the select-box, which is displayed for non-JS users who will
     * choose with this box, which item they want to have.
     *
     * @param bool $set false if we DO want a select-box, false otherwise
     *
     * @return QuickSearch
     */
    public function noSelectbox($set = true)
    {
        $this->selectBox = !$set;
        return $this;
    }

    /**
     * disables the ajax autocomplete for this searchfield
     * If you want to disable all QuickSearches, you better use the
     * config variable global -> AJAX_AUTOCOMPLETE_DISABLED
     * @param disable boolean: true (default) to disable, false to enable
     * autocomplete via ajax.
     * @return QuickSearch
     */
    public function disableAutocomplete($disable = true) {
        $this->autocomplete_disabled = $disable;
        return $this;
    }

    /**
     * set a JavaScript-function to be fired after the user has selected a
     * value in the QuickSearch field. Arguments are:
     * function fireme(id_of_item, text_of_item)
     * example setting: QS->fireJSFunctionOnSelect('fireme');
     *
     * @param string $function_name the name of the javascript function
     *
     * @return QuickSearch
     */
    public function fireJSFunctionOnSelect($function_name)
    {
        $this->jsfunction = $function_name;
        return $this;
    }

    /**
     * assigns special attributes to the html-element of the searchfield
     *
     * @param array $ttr_array like array("title" => "hello world")
     *
     * @return QuickSearch
     */
    public function setAttributes($attr_array)
    {
        if (is_array($attr_array)) {
            $this->withAttributes = $attr_array;
        }
        if (!isset($this->withAttributes['aria-label'])
                && !isset($this->withAttributes['aria-labelledby'])
                && $this->search) {
            $this->withAttributes['aria-label'] = $this->search->getTitle();
        }
        return $this;
    }

    /**
     * Returns whether the underlying search type requires an extended
     * layout.
     *
     * @return bool indicating whether an extended layout is required
     */
    public function hasExtendedLayout()
    {
        return !empty($this->search->extendedLayout);
    }

    /**
     * last step: display everything and be happy!
     * comment: the Ajax-Result (for the javascript-instant-search) will be also displayed here,
     * but that does not need to concern you.
     *
     * @return string
     */
    public function render()
    {
        if (trim(Request::get($this->name.'_parameter'))
               && (Request::get($this->name.'_parameter') != $this->beschriftung())
               && !Request::get($this->name)
               && $this->selectBox) {
            //No Javascript activated and having searched:
            $searchresults = $this->searchresults(Request::get($this->name.'_parameter'));

            $template = $GLOBALS['template_factory']->open('quicksearch/selectbox.php');
            $template->set_attribute('withButton', $this->withButton);
            $template->set_attribute('box_align', $this->box_align);
            $template->set_attribute('box_width', $this->box_width);
            $template->set_attribute('withAttributes', $this->withAttributes);
            $template->set_attribute('searchresults', $searchresults);
            $template->set_attribute('name', $this->name);
            $template->set_attribute('search_button_name', $this->search_button_name);
            $template->set_attribute('reset_button_name', $this->reset_button_name);
            $template->set_attribute('extendedLayout', $this->hasExtendedLayout());
            return $template->render();

        } else {
            $query_id = $this->storeSearchInSession();

            //Ausgabe:
            $template = $GLOBALS['template_factory']->open('quicksearch/inputfield.php');
            $template->set_attribute('withButton', $this->withButton);
            $template->set_attribute('box_align', $this->box_align);
            $template->set_attribute('box_width', $this->box_width);
            $template->set_attribute('inputStyle', $this->inputStyle ?? '');
            $template->set_attribute('beschriftung', $this->beschriftung());
            $template->set_attribute('name', $this->name);
            $template->set_attribute('defaultID', $this->defaultID);
            $template->set_attribute('defaultName', $this->defaultName);
            $template->set_attribute('withAttributes', $this->withAttributes ? $this->withAttributes : []);
            $template->set_attribute('jsfunction', $this->jsfunction);
            $template->set_attribute('autocomplete_disabled', Config::get()->getValue("AJAX_AUTOCOMPLETE_DISABLED") || $this->autocomplete_disabled);
            $template->set_attribute('count_QS', self::$count_QS);
            $template->set_attribute('id', $this->getId());
            $template->set_attribute('query_id', $query_id);
            $template->set_attribute('minLength', $this->minLength);
            $template->set_attribute('search_button_name', $this->search_button_name);
            $template->set_attribute('reset_button_name', $this->reset_button_name);
            $template->set_attribute('extendedLayout', $this->hasExtendedLayout());
            return $template->render();
        }
    }

    /**
     * Convert quicksearch to string by rendering it
     *
     * @return string rendered html
     */
    public function __toString()
    {
        return $this->render();
    }

    /**
     * returns the id string used for the input field
     *
     * @return string
     */
    public function getId()
    {
        return "qs_".md5($this->name) . '_' . (int)self::$count_QS;
    }

    //////////////////////////////////////////////////////////////////////////////
    //                               private-methods                            //
    //////////////////////////////////////////////////////////////////////////////

    /**
     * private method to get a result-array in the way of array(array(item_id, item-name)).
     *
     * @param string $request the request from the searchfield typed by the user.
     *
     * @return array array(array(item_id, item-name), ...).
     */
    private function searchresults($request)
    {
        if ($this->search instanceof SearchType) {
            try {
                $results = $this->search->getResults($request, $_REQUEST);
            } catch (Exception $exception) {
                //Der Programmierer will ja seine Fehler sehen:
                return [["", $exception->getMessage()]];
            }
            return $results;
        } else {
            $result = [["", _("Kein korrektes Suchobjekt angegeben.")]];
            return $result;
        }
    }

    /**
     * get the label of the searchfield that is written in javascript and disappears
     * when the user focusses on the searchfield.
     *
     * @return string localized-string
     */
    private function beschriftung()
    {
        if ($this->search instanceof SearchType) {
            return $this->search->getTitle();
        } else {
            return "";
        }
    }

    /**
     * Abfrage in der Session speichern
     *
     * @return string
     */
    protected function storeSearchInSession(): string
    {
        $query_id = md5(serialize($this->search));

        // Prepare object
        $item = [
            'time' => time(),
        ];

        if ($this->search instanceof SearchType) {
            $item['object'] = serialize($this->search);
            if ($this->search instanceof SearchType) {
                $item['includePath'] = $this->search->includePath();
            }
        } else {
            $item['query'] = $this->search;
        }

        // Actually storing in session
        if (!isset($_SESSION['QuickSearches'])) {
            $_SESSION['QuickSearches'] = [];
        }
        $_SESSION['QuickSearches'][$query_id] = $item;

        return $query_id;
    }
}