Browse Source

Merge branch 'logging'

* logging:
  added JavaScript based filter mechanism
  added logging configuration
  replaced out calls to dbglog with new Logger calls
  added convenience methods to log to our default facilities
  added logviwer admin plugin
  added log dir to git
  central logging mechanism
pull/3355/head
Andreas Gohr 5 months ago
parent
commit
413313a155
  1. 1
      .gitignore
  2. 1
      conf/dokuwiki.php
  3. 1
      data/log/_dummy
  4. 3
      inc/Debug/DebugHelper.php
  5. 20
      inc/ErrorHandler.php
  6. 8
      inc/Extension/Event.php
  7. 164
      inc/Logger.php
  8. 14
      inc/Sitemap/Mapper.php
  9. 2
      inc/Ui/Admin.php
  10. 35
      inc/infoutils.php
  11. 23
      inc/init.php
  12. 4
      lib/plugins/authad/auth.php
  13. 1
      lib/plugins/config/lang/en/lang.php
  14. 8
      lib/plugins/config/settings/config.metadata.php
  15. 2
      lib/plugins/extension/helper/extension.php
  16. 152
      lib/plugins/logviewer/admin.php
  17. 1
      lib/plugins/logviewer/admin.svg
  18. 8
      lib/plugins/logviewer/lang/en/intro.txt
  19. 10
      lib/plugins/logviewer/lang/en/lang.php
  20. 1
      lib/plugins/logviewer/lang/en/nolog.txt
  21. 7
      lib/plugins/logviewer/plugin.info.txt
  22. 25
      lib/plugins/logviewer/script.js
  23. 51
      lib/plugins/logviewer/style.less
  24. 9
      lib/scripts/behaviour.js

1
.gitignore

@ -56,6 +56,7 @@
!/lib/plugins/styling
!/lib/plugins/testing
!/lib/plugins/usermanager
!/lib/plugins/logviewer
!/lib/plugins/action.php
!/lib/plugins/admin.php
!/lib/plugins/auth.php

1
conf/dokuwiki.php

@ -111,6 +111,7 @@ $conf['mailfrom'] = ''; //use this email when sending mails
$conf['mailreturnpath'] = ''; //use this email as returnpath for bounce mails
$conf['mailprefix'] = ''; //use this as prefix of outgoing mails
$conf['htmlmail'] = 1; //send HTML multipart mails
$conf['dontlog'] = 'debug'; //logging facilites that should be disabled
/* Syndication Settings */
$conf['sitemap'] = 0; //Create a google sitemap? How often? In days.

1
data/log/_dummy

@ -0,0 +1 @@
You can safely delete this file.

3
inc/Debug/DebugHelper.php

@ -5,6 +5,7 @@ namespace dokuwiki\Debug;
use Doku_Event;
use dokuwiki\Extension\EventHandler;
use dokuwiki\Logger;
class DebugHelper
{
@ -164,7 +165,7 @@ class DebugHelper
if ($event->data['alternative']) {
$msg .= ' ' . $event->data['alternative'] . ' should be used instead!';
}
dbglog($msg);
Logger::getInstance(Logger::LOG_DEPRECATED)->log($msg);
}
$event->advise_after();
}

20
inc/ErrorHandler.php

@ -36,7 +36,7 @@ class ErrorHandler
$msg = 'An unforeseen error has occured. This is most likely a bug somewhere.';
if ($plugin) $msg .= ' It might be a problem in the ' . $plugin . ' plugin.';
$logged = self::logException($e)
? 'More info has been written to the DokuWiki _error.log'
? 'More info has been written to the DokuWiki error log.'
: $e->getFile() . ':' . $e->getLine();
echo <<<EOT
@ -64,7 +64,7 @@ EOT;
public static function showExceptionMsg($e, $intro = 'Error!')
{
$msg = hsc($intro) . '<br />' . hsc(get_class($e) . ': ' . $e->getMessage());
if (self::logException($e)) $msg .= '<br />More info is available in the _error.log';
if (self::logException($e)) $msg .= '<br />More info is available in the error log.';
msg($msg, -1);
}
@ -100,16 +100,12 @@ EOT;
*/
public static function logException($e)
{
global $conf;
$log = join("\t", [
gmdate('c'),
get_class($e),
$e->getFile() . '(' . $e->getLine() . ')',
$e->getMessage(),
]) . "\n";
$log .= $e->getTraceAsString() . "\n";
return io_saveFile($conf['cachedir'] . '/_error.log', $log, true);
return Logger::getInstance()->log(
get_class($e) . ': ' . $e->getMessage(),
$e->getTraceAsString(),
$e->getFile(),
$e->getLine()
);
}
/**

8
inc/Extension/Event.php

@ -3,6 +3,8 @@
namespace dokuwiki\Extension;
use dokuwiki\Logger;
/**
* The Action plugin event
*/
@ -71,7 +73,8 @@ class Event
if ($EVENT_HANDLER !== null) {
$EVENT_HANDLER->process_event($this, 'BEFORE');
} else {
dbglog($this->name . ':BEFORE event triggered before event system was initialized');
Logger::getInstance(Logger::LOG_DEBUG)
->log($this->name . ':BEFORE event triggered before event system was initialized');
}
return (!$enablePreventDefault || $this->runDefault);
@ -92,7 +95,8 @@ class Event
if ($EVENT_HANDLER !== null) {
$EVENT_HANDLER->process_event($this, 'AFTER');
} else {
dbglog($this->name . ':AFTER event triggered before event system was initialized');
Logger::getInstance(Logger::LOG_DEBUG)->
log($this->name . ':AFTER event triggered before event system was initialized');
}
}

164
inc/Logger.php

@ -0,0 +1,164 @@
<?php
namespace dokuwiki;
class Logger
{
const LOG_ERROR = 'error';
const LOG_DEPRECATED = 'deprecated';
const LOG_DEBUG = 'debug';
/** @var Logger[] */
static protected $instances;
/** @var string what kind of log is this */
protected $facility;
protected $isLogging = true;
/**
* Logger constructor.
*
* @param string $facility The type of log
*/
protected function __construct($facility)
{
global $conf;
$this->facility = $facility;
// Should logging be disabled for this facility?
$dontlog = explode(',', $conf['dontlog']);
$dontlog = array_map('trim', $dontlog);
if (in_array($facility, $dontlog)) $this->isLogging = false;
}
/**
* Return a Logger instance for the given facility
*
* @param string $facility The type of log
* @return Logger
*/
static public function getInstance($facility = self::LOG_ERROR)
{
if (self::$instances[$facility] === null) {
self::$instances[$facility] = new Logger($facility);
}
return self::$instances[$facility];
}
/**
* Convenience method to directly log to the error log
*
* @param string $message The log message
* @param mixed $details Any details that should be added to the log entry
* @param string $file A source filename if this is related to a source position
* @param int $line A line number for the above file
* @return bool has a log been written?
*/
static public function error($message, $details = null, $file = '', $line = 0)
{
return self::getInstance(self::LOG_ERROR)->log(
$message, $details, $file, $line
);
}
/**
* Convenience method to directly log to the debug log
*
* @param string $message The log message
* @param mixed $details Any details that should be added to the log entry
* @param string $file A source filename if this is related to a source position
* @param int $line A line number for the above file
* @return bool has a log been written?
*/
static public function debug($message, $details = null, $file = '', $line = 0)
{
return self::getInstance(self::LOG_DEBUG)->log(
$message, $details, $file, $line
);
}
/**
* Convenience method to directly log to the deprecation log
*
* @param string $message The log message
* @param mixed $details Any details that should be added to the log entry
* @param string $file A source filename if this is related to a source position
* @param int $line A line number for the above file
* @return bool has a log been written?
*/
static public function deprecated($message, $details = null, $file = '', $line = 0)
{
return self::getInstance(self::LOG_DEPRECATED)->log(
$message, $details, $file, $line
);
}
/**
* Log a message to the facility log
*
* @param string $message The log message
* @param mixed $details Any details that should be added to the log entry
* @param string $file A source filename if this is related to a source position
* @param int $line A line number for the above file
* @return bool has a log been written?
*/
public function log($message, $details = null, $file = '', $line = 0)
{
if(!$this->isLogging) return false;
// details are logged indented
if ($details) {
if (!is_string($details)) {
$details = json_encode($details, JSON_PRETTY_PRINT);
}
$details = explode("\n", $details);
$loglines = array_map(function ($line) {
return ' ' . $line;
}, $details);
} elseif ($details) {
$loglines = [$details];
} else {
$loglines = [];
}
// datetime, fileline, message
$logline = gmdate('Y-m-d H:i:s') . "\t";
if ($file) {
$logline .= $file;
if ($line) $logline .= "($line)";
}
$logline .= "\t" . $message;
array_unshift($loglines, $logline);
return $this->writeLogLines($loglines);
}
/**
* Construct the log file for the given day
*
* @param false|string|int $date Date to access, false for today
* @return string
*/
public function getLogfile($date = false)
{
global $conf;
if ($date !== null) $date = strtotime($date);
if (!$date) $date = time();
return $conf['logdir'] . '/' . $this->facility . '/' . date('Y-m-d', $date) . '.log';
}
/**
* Write the given lines to today's facility log
*
* @param string[] $lines the raw lines to append to the log
* @return bool true if the log was written
*/
protected function writeLogLines($lines)
{
$logfile = $this->getLogfile();
return io_saveFile($logfile, join("\n", $lines) . "\n", true);
}
}

14
inc/Sitemap/Mapper.php

@ -9,6 +9,7 @@
namespace dokuwiki\Sitemap;
use dokuwiki\HTTP\DokuHTTPClient;
use dokuwiki\Logger;
/**
* A class for building sitemaps and pinging search engines with the sitemap URL.
@ -43,14 +44,14 @@ class Mapper {
if(@filesize($sitemap) &&
@filemtime($sitemap) > (time()-($conf['sitemap']*86400))){ // 60*60*24=86400
dbglog('Sitemapper::generate(): Sitemap up to date');
Logger::debug('Sitemapper::generate(): Sitemap up to date');
return false;
}
dbglog("Sitemapper::generate(): using $sitemap");
Logger::debug("Sitemapper::generate(): using $sitemap");
$pages = idx_get_indexer()->getPages();
dbglog('Sitemapper::generate(): creating sitemap using '.count($pages).' pages');
Logger::debug('Sitemapper::generate(): creating sitemap using '.count($pages).' pages');
$items = array();
// build the sitemap items
@ -150,10 +151,11 @@ class Mapper {
$event = new \dokuwiki\Extension\Event('SITEMAP_PING', $data);
if ($event->advise_before(true)) {
foreach ($data['ping_urls'] as $name => $url) {
dbglog("Sitemapper::PingSearchEngines(): pinging $name");
Logger::debug("Sitemapper::PingSearchEngines(): pinging $name");
$resp = $http->get($url);
if($http->error) dbglog("Sitemapper:pingSearchengines(): $http->error");
dbglog('Sitemapper:pingSearchengines(): '.preg_replace('/[\n\r]/',' ',strip_tags($resp)));
if($http->error) {
Logger::debug("Sitemapper:pingSearchengines(): $http->error", $resp);
}
}
}
$event->advise_after();

2
inc/Ui/Admin.php

@ -14,7 +14,7 @@ use dokuwiki\Utf8\Sort;
*/
class Admin extends Ui {
protected $forAdmins = array('usermanager', 'acl', 'extension', 'config', 'styling');
protected $forAdmins = array('usermanager', 'acl', 'extension', 'config', 'logviewer', 'styling');
protected $forManagers = array('revert', 'popularity');
/** @var array[] */
protected $menu;

35
inc/infoutils.php

@ -7,6 +7,7 @@
*/
use dokuwiki\HTTP\DokuHTTPClient;
use dokuwiki\Logger;
if(!defined('DOKU_MESSAGEURL')){
if(in_array('ssl', stream_get_transports())) {
@ -35,7 +36,7 @@ function checkUpdateMessages(){
// check if new messages needs to be fetched
if($lm < time()-(60*60*24) || $lm < @filemtime(DOKU_INC.DOKU_SCRIPT)){
@touch($cf);
dbglog("checkUpdateMessages(): downloading messages to ".$cf.($is_http?' (without SSL)':' (with SSL)'));
Logger::debug("checkUpdateMessages(): downloading messages to ".$cf.($is_http?' (without SSL)':' (with SSL)'));
$http = new DokuHTTPClient();
$http->timeout = 12;
$resp = $http->get(DOKU_MESSAGEURL.$updateVersion);
@ -44,10 +45,10 @@ function checkUpdateMessages(){
// or it looks like one of our messages, not WiFi login or other interposed response
io_saveFile($cf,$resp);
} else {
dbglog("checkUpdateMessages(): unexpected HTTP response received");
Logger::debug("checkUpdateMessages(): unexpected HTTP response received", $http->error);
}
}else{
dbglog("checkUpdateMessages(): messages up to date");
Logger::debug("checkUpdateMessages(): messages up to date");
}
$data = io_readFile($cf);
@ -429,33 +430,25 @@ function dbg($msg,$hidden=false){
}
/**
* Print info to a log file
* Print info to debug log file
*
* @author Andreas Gohr <andi@splitbrain.org>
*
* @deprecated 2020-08-13
* @param string $msg
* @param string $header
*/
function dbglog($msg,$header=''){
global $conf;
/* @var Input $INPUT */
global $INPUT;
dbg_deprecated('\\dokuwiki\\Logger');
// The debug log isn't automatically cleaned thus only write it when
// debugging has been enabled by the user.
if($conf['allowdebug'] !== 1) return;
if(is_object($msg) || is_array($msg)){
$msg = print_r($msg,true);
// was the msg as single line string? use it as header
if($header === '' && is_string($msg) && strpos($msg, "\n") === false) {
$header = $msg;
$msg = '';
}
if($header) $msg = "$header\n$msg";
$file = $conf['cachedir'].'/debug.log';
$fh = fopen($file,'a');
if($fh){
fwrite($fh,date('H:i:s ').$INPUT->server->str('REMOTE_ADDR').': '.$msg."\n");
fclose($fh);
}
Logger::getInstance(Logger::LOG_DEBUG)->log(
$header, $msg
);
}
/**

23
inc/init.php

@ -269,16 +269,19 @@ function init_session() {
function init_paths(){
global $conf;
$paths = array('datadir' => 'pages',
'olddir' => 'attic',
'mediadir' => 'media',
'mediaolddir' => 'media_attic',
'metadir' => 'meta',
'mediametadir' => 'media_meta',
'cachedir' => 'cache',
'indexdir' => 'index',
'lockdir' => 'locks',
'tmpdir' => 'tmp');
$paths = [
'datadir' => 'pages',
'olddir' => 'attic',
'mediadir' => 'media',
'mediaolddir' => 'media_attic',
'metadir' => 'meta',
'mediametadir' => 'media_meta',
'cachedir' => 'cache',
'indexdir' => 'index',
'lockdir' => 'locks',
'tmpdir' => 'tmp',
'logdir' => 'log',
];
foreach($paths as $c => $p) {
$path = empty($conf[$c]) ? $conf['savedir'].'/'.$p : $conf[$c];

4
lib/plugins/authad/auth.php

@ -1,6 +1,8 @@
<?php
use dokuwiki\Utf8\Sort;
use dokuwiki\Logger;
/**
* Active Directory authentication backend for DokuWiki
*
@ -384,7 +386,7 @@ class auth_plugin_authad extends DokuWiki_Auth_Plugin
{
$adldap = $this->initAdLdap(null);
if (!$adldap) {
dbglog("authad/auth.php getUserCount(): _adldap not set.");
Logger::debug("authad/auth.php getUserCount(): _adldap not set.");
return -1;
}
if ($filter == array()) {

1
lib/plugins/config/lang/en/lang.php

@ -147,6 +147,7 @@ $lang['mailfrom'] = 'Sender email address to use for automatic mails';
$lang['mailreturnpath'] = 'Recipient email address for non delivery notifications';
$lang['mailprefix'] = 'Email subject prefix to use for automatic mails. Leave blank to use the wiki title';
$lang['htmlmail'] = 'Send better looking, but larger in size HTML multipart emails. Disable for plain text only mails.';
$lang['dontlog'] = 'Disable logging for these types of logs.';
/* Syndication Settings */
$lang['sitemap'] = 'Generate Google sitemap this often (in days). 0 to disable';

8
lib/plugins/config/settings/config.metadata.php

@ -199,6 +199,14 @@ $meta['mailfrom'] = array('email', '_placeholders' => true);
$meta['mailreturnpath'] = array('email', '_placeholders' => true);
$meta['mailprefix'] = array('string');
$meta['htmlmail'] = array('onoff');
$meta['dontlog'] = array(
'disableactions',
'_choices' => array(
'error',
'debug',
'deprecated',
),
);
$meta['_syndication'] = array('fieldset');
$meta['sitemap'] = array('numeric');

2
lib/plugins/extension/helper/extension.php

@ -128,7 +128,7 @@ class helper_plugin_extension_extension extends DokuWiki_Plugin
array(
'authad', 'authldap', 'authpdo', 'authplain',
'acl', 'config', 'extension', 'info', 'popularity', 'revert',
'safefnrecode', 'styling', 'testing', 'usermanager',
'safefnrecode', 'styling', 'testing', 'usermanager', 'logviewer',
'template:dokuwiki',
)
);

152
lib/plugins/logviewer/admin.php

@ -0,0 +1,152 @@
<?php
use dokuwiki\Logger;
/**
* DokuWiki Plugin logviewer (Admin Component)
*
* @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
* @author Andreas Gohr <andi@splitbrain.org>
*/
class admin_plugin_logviewer extends DokuWiki_Admin_Plugin
{
protected $facilities;
protected $facility;
protected $date;
/** @inheritDoc */
public function forAdminOnly()
{
return true;
}
/** @inheritDoc */
public function handle()
{
global $INPUT;
$this->facilities = $this->getFacilities();
$this->facility = $INPUT->str('facility');
if (!in_array($this->facility, $this->facilities)) {
$this->facility = $this->facilities[0];
}
$this->date = $INPUT->str('date');
if (!preg_match('/^\d\d\d\d-\d\d-\d\d$/', $this->date)) {
$this->date = gmdate('Y-m-d');
}
}
/** @inheritDoc */
public function html()
{
echo '<div id="plugin__logviewer">';
echo $this->locale_xhtml('intro');
$this->displayTabs();
$this->displayLog();
echo '</div>';
}
/**
* Show the navigational tabs and date picker
*/
protected function displayTabs()
{
global $ID;
$form = new dokuwiki\Form\Form(['method'=>'GET']);
$form->setHiddenField('do', 'admin');
$form->setHiddenField('page', 'logviewer');
$form->setHiddenField('facility', $this->facility);
$form->addTextInput('date',$this->getLang('date'))->attr('type','date')->val($this->date)->addClass('quickselect');
$form->addButton('submit','>')->attr('type','submit');
echo $form->toHTML();
echo '<ul class="tabs">';
foreach ($this->facilities as $facility) {
echo '<li>';
if ($facility == $this->facility) {
echo '<strong>' . hsc($facility) . '</strong>';
} else {
$link = wl($ID,
['do' => 'admin', 'page' => 'logviewer', 'date' => $this->date, 'facility' => $facility]);
echo '<a href="' . $link . '">' . hsc($facility) . '</a>';
}
echo '</li>';
}
echo '</ul>';
}
/**
* Output the logfile contents
*/
protected function displayLog()
{
$logfile = Logger::getInstance($this->facility)->getLogfile($this->date);
if (!file_exists($logfile)) {
echo $this->locale_xhtml('nolog');
return;
}
// loop through the file an print it
echo '<dl>';
$lines = file($logfile);
$cnt = count($lines);
for ($i = 0; $i < $cnt; $i++) {
$line = $lines[$i];
if ($line[0] === ' ' && $line[1] === ' ') {
// lines indented by two spaces are details, aggregate them
echo '<dd>';
while ($line[0] === ' ' && $line[1] === ' ') {
echo hsc(substr($line, 2)) . '<br />';
$line = $lines[$i++];
}
echo '</dd>';
$i -= 2; // rewind the counter
} else {
// other lines are actual log lines in three parts
list($dt, $file, $msg) = explode("\t", $line, 3);
echo '<dt>';
echo '<span class="datetime">' . hsc($dt) . '</span>';
echo '<span class="log">';
echo '<span class="msg">' . hsc($msg) . '</span>';
echo '<span class="file">' . hsc($file) . '</span>';
echo '</span>';
echo '</dt>';
}
}
echo '</dl>';
}
/**
* Get the available logging facilities
*
* @return array
*/
protected function getFacilities()
{
global $conf;
$conf['logdir'];
// default facilities first
$facilities = [
Logger::LOG_ERROR,
Logger::LOG_DEPRECATED,
Logger::LOG_DEBUG,
];
// add all other dirs
$dirs = glob($conf['logdir'] . '/*', GLOB_ONLYDIR);
foreach ($dirs as $dir) {
$facilities[] = basename($dir);
}
$facilities = array_unique($facilities);
return $facilities;
}
}

1
lib/plugins/logviewer/admin.svg

@ -0,0 +1 @@
<svg viewBox="0 0 24 24"><path d="M15 20a1 1 0 0 0 1-1V4H8a1 1 0 0 0-1 1v11H5V5a3 3 0 0 1 3-3h11a3 3 0 0 1 3 3v1h-2V5a1 1 0 0 0-1-1 1 1 0 0 0-1 1v14a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-1h11a2 2 0 0 0 2 2M9 6h5v2H9V6m0 4h5v2H9v-2m0 4h5v2H9v-2z"/></svg>

8
lib/plugins/logviewer/lang/en/intro.txt

@ -0,0 +1,8 @@
====== View Logs ======
This interface allows you to view the various logs that are written by DokuWiki. By default, there shouldn't be logged
much here (it depends on your [[doku>config:dontlog|log settings]]). However if something goes wrong, chances are high,
you'll find useful info here. All times are UTC!
Please be aware **log files can contain sensitive information** like passwords, paths or other secrets.
Be sure to redact the logs appropriately when posting them on the forum or in bug reports!

10
lib/plugins/logviewer/lang/en/lang.php

@ -0,0 +1,10 @@
<?php
/**
* English language file for logviewer plugin
*
* @author Andreas Gohr <andi@splitbrain.org>
*/
$lang['menu'] = 'View Logs';
$lang['date'] = 'Date';
$lang['js']['filter'] = 'Filter Loglines:';

1
lib/plugins/logviewer/lang/en/nolog.txt

@ -0,0 +1 @@
There are no log entries for the selected day and log facility.

7
lib/plugins/logviewer/plugin.info.txt

@ -0,0 +1,7 @@
base logviewer
author Andreas Gohr
email andi@splitbrain.org
date 2020-08-13
name logviewer plugin
desc View DokuWiki logs
url https://www.dokuwiki.org/plugin:logviewer

25
lib/plugins/logviewer/script.js

@ -0,0 +1,25 @@
/**
* Scroll to the end of the log on load
*/
jQuery(function () {
var $dl = jQuery('#plugin__logviewer').find('dl');
if (!$dl.length) return;
$dl.animate({scrollTop: $dl.prop("scrollHeight")}, 500);
var $filter = jQuery('<input>');
$filter.on('keyup', function (e) {
var re = new RegExp($filter.val(), 'i');
$dl.find('dt').each(function (idx, elem) {
if (elem.innerText.match(re)) {
jQuery(elem).removeClass('hidden');
} else {
jQuery(elem).addClass('hidden');
}
});
});
$dl.before($filter);
$filter.wrap('<label></label>');
$filter.before(LANG.plugins.logviewer.filter + ' ');
});

51
lib/plugins/logviewer/style.less

@ -0,0 +1,51 @@
#plugin__logviewer {
form {
float: right;
}
.tabs {
margin-bottom: 2em;
}
label {
display: block;
margin-top: -1em;
margin-bottom: 1em;
}
dl {
max-height: 80vh;
overflow: auto;
dt {
display: flex;
&.hidden {
display: none;
}
.datetime {
flex: 0 0 auto;
margin-right: 1em;
}
.log {
flex: 1 1 auto;
span {
display: block;
}
span.file {
font-family: monospace;
}
}
}
dd {
font-size: 80%;
white-space: nowrap;
font-family: monospace;
}
}
}

9
lib/scripts/behaviour.js

@ -114,15 +114,18 @@ var dw_behaviour = {
/**
* Autosubmit quick select forms
*
* When a <select> tag has the class "quickselect", this script will
* When a <select> or <input> tag has the class "quickselect", this script will
* automatically submit its parent form when the select value changes.
* It also hides the submit button of the form.
*
* This includes a workaround a weird behaviour when the submit button has a name
*
* @link https://trackjs.com/blog/when-form-submit-is-not-a-function/
* @author Andreas Gohr <andi@splitbrain.org>
*/
quickSelect: function(){
jQuery('select.quickselect')
.on('change', function(e){ e.target.form.submit(); })
jQuery('.quickselect')
.change(function(e){ HTMLFormElement.prototype.submit.call(e.target.form); })
.closest('form').find(':button').not('.show').hide();
},

Loading…
Cancel
Save