<?php
/*
 * This code is part of GOsa (http://www.gosa-project.org)
 * Copyright (C) 2003-2008 GONICUS GmbH
 *
 * ID: $$Id$$
 *
 * 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.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

class listing
{
    public $xmlData;
    public $entries;
    public $departments = [];
    public $departmentBrowser = false;
    public $departmentRootVisible = false;
    public $multiSelect = false;
    public $singleSelect = false;
    public $noAclChecks = false;
    public $template;
    public $headline;
    public $base;
    public $sortDirection = null;
    public $sortColumn = null;
    public $sortAttribute;
    public $sortType;
    public $numColumns;
    public $baseMode = false;
    public $bases = [];
    public $header = [];
    public $colprops = [];
    public $filters = [];
    public $filter = null;
    public $pid;
    public $objectTypes = [];
    public $objectTypeCount = [];
    public $objectDnMapping = [];
    public $copyPasteHandler = null;
    public $snapshotHandler = null;
    public $exporter = [];
    public $exportColumns = [];
    public $useSpan = false;
    public $height = 0;
    public $scrollPosition = 0;
    public $baseSelector;
    public $aclToObjectClass = [];

    public $theme = "";

    public function __construct($source, $isString = false)
    {
        global $config;
        global $class_mapping;

        $this->theme = getThemeName();

        // Initialize pid
        $this->pid = preg_replace('/[^0-9]/', '', microtime(true));

        if ($isString) {
            if (!$this->loadString($source)) {
                exit("Cannot parse $source!");
            }
        } else {
            if (!$this->loadFile($source)) {
                exit("Cannot parse $source!");
            }
        }

        // Set base for filter
        if ($this->baseMode) {
            $this->base = session::global_get('CurrentMainBase');
            if ($this->base == null) {
                $this->base = $config->current['BASE'];
            }
            $this->refreshBasesList();
        } else {
            $this->base = $config->current['BASE'];
        }

        // Move footer information
        $this->showFooter = ($config->get_cfg_value('core', 'listSummary') == 'true');

        // Register build in filters
        $this->registerElementFilter('objectType', 'listing::filterObjectType');
        $this->registerElementFilter('departmentLink', 'listing::filterDepartmentLink');
        $this->registerElementFilter('link', 'listing::filterLink');
        $this->registerElementFilter('actions', 'listing::filterActions');

        // Load exporters
        foreach ($class_mapping as $class => $dummy) {
            if (preg_match('/Exporter$/', $class)) {
                $info = call_user_func([$class, 'getInfo']);
                if ($info != null) {
                    $this->exporter = array_merge($this->exporter, $info);
                }
            }
        }

        // Instanciate base selector
        $this->baseSelector = new baseSelector($this->bases, $this->base);
    }

    public function setCopyPasteHandler($handler)
    {
        $this->copyPasteHandler = &$handler;
    }

    public function setHeight($height)
    {
        $this->height = $height;
    }

    public function setSnapshotHandler($handler)
    {
        $this->snapshotHandler = &$handler;
    }

    public function getFilter()
    {
        return $this->filter;
    }

    public function setFilter($filter)
    {
        $this->filter = &$filter;
        if ($this->departmentBrowser) {
            $this->departments = $this->getDepartments();
        }
        $this->filter->setBase($this->base);
    }

    public function registerElementFilter($name, $call)
    {
        if (!isset($this->filters[$name])) {
            $this->filters[$name] = $call;

            return true;
        }

        return false;
    }

    public function loadFile($filename)
    {
        return $this->loadString(file_get_contents($filename));
    }

    public function loadString($contents)
    {
        $this->xmlData = xml::xml2array($contents, 1);

        if (!isset($this->xmlData['list'])) {
            return false;
        }

        $this->xmlData = $this->xmlData['list'];

        // Load some definition values
        foreach (['departmentBrowser', 'departmentRootVisible', 'multiSelect', 'singleSelect', 'baseMode', 'noAclChecks'] as $token) {
            if (isset($this->xmlData['definition'][$token]) &&
                    $this->xmlData['definition'][$token] == 'true') {
                $this->$token = true;
            }
        }

        // Fill objectTypes from departments and xml definition
        $types = departmentManagement::get_support_departments();
        foreach ($types as $class => $data) {
            $this->objectTypes[$data['OC']] = ['label' => $data['TITLE'],
                    'objectClass' => $data['OC'],
                    'image' => $data['IMG'], ];
        }
        $this->categories = [];
        if (isset($this->xmlData['definition']['objectType'])) {
            if (isset($this->xmlData['definition']['objectType']['label'])) {
                $this->xmlData['definition']['objectType'] = [$this->xmlData['definition']['objectType']];
            }
            foreach ($this->xmlData['definition']['objectType'] as $index => $otype) {
                $tmp = $this->xmlData['definition']['objectType'][$index];
                $this->objectTypes[$tmp['objectClass']] = $tmp;
                if (isset($this->xmlData['definition']['objectType'][$index]['category'])) {
                    $this->categories[] = $otype['category'];

                    if (isset($otype['category']) && isset($otype['class'])) {
                        $this->aclToObjectClass[$otype['category'].'/'.$otype['class']][] = $otype['objectClass'];
                    }
                }
            }
        }
        $this->objectTypes = array_values($this->objectTypes);

        // Parse layout per column
        $this->colprops = $this->parseLayout($this->xmlData['table']['layout']);

        // Prepare table headers
        $this->renderHeader();

        // Assign headline/Categories
        $this->headline = _($this->xmlData['definition']['label']);
        if (!is_array($this->categories)) {
            $this->categories = [$this->categories];
        }

        // Evaluate columns to be exported
        if (isset($this->xmlData['table']['column'])) {
            foreach ($this->xmlData['table']['column'] as $index => $config) {
                if (isset($config['export']) && $config['export'] == 'true') {
                    $this->exportColumns[] = $index;
                }
            }
        }

        return true;
    }

    public function renderHeader()
    {
        $this->header = [];
        $this->plainHeader = [];

        // Initialize sort?
        $sortInit = false;
        if (!$this->sortDirection) {
            $this->sortColumn = 0;
            if (isset($this->xmlData['definition']['defaultSortColumn'])) {
                $this->sortColumn = $this->xmlData['definition']['defaultSortColumn'];
            } else {
                $this->sortAttribute = '';
            }
            $this->sortDirection = [];
            $sortInit = true;
        }

        if (isset($this->xmlData['table']['column'])) {
            foreach ($this->xmlData['table']['column'] as $index => $conf) {
                // Initialize everything to one direction
                if ($sortInit) {
                    $this->sortDirection[$index] = false;
                }

                $sorter = '';
                if ($index == $this->sortColumn && isset($conf['sortAttribute']) &&
                        isset($conf['sortType'])) {
                    $this->sortAttribute = $conf['sortAttribute'];
                    $this->sortType = $conf['sortType'];
                    switch ($this->theme) {
                        case 'classic':
                            $sorter = '&nbsp;'.image('images/lists/sort-'.($this->sortDirection[$index] ? 'up' : 'down').'.png', null, $this->sortDirection[$index] ? _('Sort ascending') : _('Sort descending'), 'text-top');
                            break;
                        default:
                            $sorter = image("<i class='material-icons'>keyboard_arrow_".($this->sortDirection[$index] ? 'up' : 'down').'</i>', null, $this->sortDirection[$index] ? _('Sort ascending') : _('Sort descending'), 'text-top ');
                            break;
                    }
                }
                $sortable = (isset($conf['sortAttribute']));

                $link = "href='?plug=".$_GET['plug']."&amp;PID=".$this->pid."&amp;act=SORT_$index'";
                if (isset($conf['label'])) {
                    switch ($this->theme) {
                        case 'classic':
                            if ($sortable) {
                                $this->header[$index] = "<td class='listheader' ".$this->colprops[$index]."><a $link>"._($conf['label'])."</a>$sorter</td>";
                            } else {
                                $this->header[$index] = "<td class='listheader' ".$this->colprops[$index].'>'._($conf['label']).'</td>';
                            }
                            break;
                        default:
                            if ($sortable) {
                                $this->header[$index] = " <th><a class='btn-flat' $link>"._($conf['label'])."</a>$sorter</th>";
                            } else {
                                $this->header[$index] = ' <th>'._($conf['label']).'</th>';
                            }
                            break;
                    }
                    $this->plainHeader[] = _($conf['label']);
                } else {
                    switch ($this->theme) {
                        case 'classic':
                            if ($sortable) {
                                $this->header[$index] = "<td class='listheader' ".$this->colprops[$index]."><a $link>&nbsp;</a>$sorter</td>";
                            } else {
                                $this->header[$index] = "<td class='listheader' ".$this->colprops[$index].'>&nbsp;</td>';
                            }
                            break;
                        default:
                            if ($sortable) {
                                $this->header[$index] = "  <th><a class='btn-flat' $link></a>$sorter</th>";
                            } else {
                                $this->header[$index] = '  <th class="icon-column"></th>';
                            }
                            break;
                    }

                    $this->plainHeader[] = '';
                }
            }
        }
    }

    public function render()
    {
        // Check for exeeded sizelimit
        if (($message = check_sizelimit()) != '') {
            return $message;
        }

        // Some browsers don't have the ability do do scrollable table bodies, filter them
        // here.
        $switch = false;
        if (preg_match('/(Opera|Konqueror|Safari)/i', $_SERVER['HTTP_USER_AGENT'])) {
            $switch = true;
        }

        // Initialize list
        $result = "<input type='hidden' value='$this->pid' name='PID'>\n";
        $result .= "<input type='hidden' name='position_".$this->pid."' id='position_".$this->pid."'>\n";
        $height = 450;
        if ($this->height != 0) {
            $result .= "<input type='hidden' value='$this->height' id='d_height'>\n";
            $height = $this->height;
        }

        switch ($this->theme) {
            case 'classic':
                $result .= "<div class='listContainer' id='d_scrollbody' style='height:{$height}px; overflow: auto;min-height:".($height + 25)."px;'>\n";
                $result .= "<table summary='$this->headline' style='width:100%;table-layout:fixed' cellspacing='0' cellpadding='0' id='t_scrolltable'>\n";
                $this->numColumns = count($this->colprops) + (($this->multiSelect | $this->singleSelect) ? 1 : 0);

                // Build list header
                $result .= "<thead class='fixedListHeader listHeaderFormat'><tr>\n";
                if ($this->multiSelect || $this->singleSelect) {
                    $width = '24px';
                    if (preg_match('/Konqueror/i', $_SERVER['HTTP_USER_AGENT'])) {
                        $width = '28px';
                    }
                    $result .= "<td class='listheader' style='text-align:center;padding:0;width:$width;'>";
                    if ($this->multiSelect) {
                        $result .= "<input type='checkbox' id='select_all' name='select_all'
                            title='"._('Select all')."' onClick='toggle_all_(\"listing_selected_[0-9]*$\",\"select_all\");' >";
                    } else {
                        $result .= '&nbsp;';
                    }
                    $result .= "</td>\n";
                }
                foreach ($this->header as $header) {
                    $result .= $header;
                }
                $result .= "</tr></thead>\n";

                // Build list body
                $result .= "<tbody class='listScrollContent listBodyFormat' id='t_nscrollbody' style='height:".$height."px;'>\n";

                // No results? Just take an empty colspanned row
                if (count($this->entries) + count($this->departments) == 0) {
                    $result .= "<tr><td class='list1nohighlight' colspan='$this->numColumns' style='height:100%;border-right:0px;width:100%;'>&nbsp;</td></tr>";
                }

                // Line color alternation
                $alt = 0;
                $deps = 0;

                // Draw department browser if configured and we're not in sub mode
                $this->useSpan = false;
                if ($this->departmentBrowser && $this->filter->scope != 'sub') {
                    // Fill with department browser if configured this way
                    $departmentIterator = new departmentSortIterator($this->departments, $this->sortDirection[$this->sortColumn]);
                    foreach ($departmentIterator as $row => $entry) {
                        $rowResult = '<tr>';

                        // Render multi select if needed
                        if ($this->multiSelect || $this->singleSelect) {
                            $rowResult .= "<td style='text-align:center;padding:0;' class='list1'>&nbsp;</td>";
                        }

                        // Render defined department columns, fill the rest with some stuff
                        $rest = $this->numColumns - 1;
                        foreach ($this->xmlData['table']['department'] as $index => $conf) {
                            $colspan = 1;
                            if (isset($conf['span'])) {
                                $colspan = $conf['span'];
                                $this->useSpan = true;
                            }
                            $rowResult .= "<td colspan='$colspan' ".$this->colprops[$index]." class='list1'>".$this->renderCell($conf['value'], $entry, $row).'</td>';
                            $rest -= $colspan;
                        }

                        // Fill remaining cols with nothing
                        $last = $this->numColumns - $rest;
                        for ($i = 0; $i < $rest; ++$i) {
                            $rowResult .= '<td '.$this->colprops[$last + $i - 1]." class='list1'>&nbsp;</td>";
                        }
                        $rowResult .= '</tr>';

                        // Apply label to objecttype icon?
                        if (preg_match("/<objectType:([^:]+):(.*)\/>/i", $rowResult, $matches)) {
                            $objectType = image($matches[1], null, LDAP::fix(base64_decode($matches[2])));
                            $rowResult = preg_replace('/<objectType[^>]+>/', $objectType, $rowResult);
                        }
                        $result .= $rowResult;
                        ++$alt;
                    }

                    $deps = $alt;
                }

                // Fill with contents, sort as configured
                $ui = get_userinfo();
                foreach ($this->entries as $row => $entry) {
                    $trow = '';

                    // Render multi select if needed
                    if ($this->multiSelect) {
                        $trow .= "<td style='text-align:center;width:20px;padding:0;' class='list0'><input type='checkbox' id='listing_selected_$row' name='listing_selected_$row'></td>\n";
                    }

                    if ($this->singleSelect) {
                        $trow .= "<td style='text-align:center;width:20px;padding:0;' class='list0'><input type='radio' id='listing_radio_selected_$row' name='listing_radio_selected' value='{$row}'></td>\n";
                    }

                    foreach ($this->xmlData['table']['column'] as $index => $conf) {
                        $renderedCell = $this->renderCell($conf['value'], $entry, $row);
                        $trow .= '<td '.$this->colprops[$index]." class='list0'>".$renderedCell."</td>\n";

                        // Save rendered column
                        $sort = preg_replace('/.*>([^<]+)<.*$/', '$1', $renderedCell);
                        $sort = preg_replace('/&nbsp;/', '', $sort);
                        if (preg_match('/</', $sort)) {
                            $sort = '';
                        }
                        $this->entries[$row]["_sort$index"] = $sort;
                    }

                    // Save rendered entry
                    $this->entries[$row]['_rendered'] = $trow;
                }

                // Complete list by sorting entries for _sort$index and appending them to the output
                $entryIterator = new listingSortIterator($this->entries, $this->sortDirection[$this->sortColumn], '_sort'.$this->sortColumn, $this->sortType);
                foreach ($entryIterator as $row => $entry) {
                    // Apply label to objecttype icon?
                    if (preg_match("/<objectType:([^:]+):(.*)\/>/i", $entry['_rendered'], $matches)) {
                        if (preg_match("/<rowLabel:([a-z0-9_-]*)\/>/i", $entry['_rendered'], $m)) {
                            $objectType = image($matches[1].'['.$m[1].']', null, LDAP::fix(base64_decode($matches[2])));
                        } else {
                            $objectType = image($matches[1], null, LDAP::fix(base64_decode($matches[2])));
                        }
                        $entry['_rendered'] = preg_replace('/<objectType[^>]+>/', $objectType, $entry['_rendered']);
                        $entry['_rendered'] = preg_replace('/<rowLabel[^>]+>/', '', $entry['_rendered']);
                    }

                    // Apply custom class to row?
                    if (preg_match("/<rowClass:([a-z0-9_-]*)\/>/i", $entry['_rendered'], $matches)) {
                        $result .= "<tr class='".$matches[1]."'>\n";
                        $result .= preg_replace('/<rowClass[^>]+>/', '', $entry['_rendered']);
                    } else {
                        $result .= "<tr>\n";
                        $result .= $entry['_rendered'];
                    }

                    $result .= "</tr>\n";
                    ++$alt;
                }

                // Need to fill the list if it's not full (nobody knows why this is 22 ;-))
                $emptyListStyle = (count($this->entries) + (($this->useSpan && count($this->entries)) ? $deps : 0) == 0) ? 'border:0;' : '';
                if ((count($this->entries) + $deps) < 22) {
                    $result .= '<tr>';
                    for ($i = 0; $i < $this->numColumns; ++$i) {
                        if ($i == 0) {
                            $result .= "<td class='list1nohighlight' style='$emptyListStyle height: 100%;'>&nbsp;</td>";
                            continue;
                        }
                        if ($i != $this->numColumns - 1) {
                            $result .= "<td class='list1nohighlight' style='$emptyListStyle '>&nbsp;</td>";
                        } else {
                            $result .= "<td class='list1nohighlight' style='border-right:0;$emptyListStyle'>&nbsp;</td>";
                        }
                    }

                    $result .= '</tr>';
                }

                // Close list body
                $result .= '</tbody></table></div>';

                // Add the footer if requested
                if ($this->showFooter) {
                    $result .= "<div class='nlistFooter'><div style='padding:3px'>";
                    foreach ($this->objectTypes as $objectType) {
                        if (isset($this->objectTypeCount[$objectType['label']])) {
                            $label = _($objectType['label']);
                            $result .= image($objectType['image'], null, $label).'&nbsp;'.$this->objectTypeCount[$objectType['label']].'&nbsp;&nbsp;';
                        } elseif (isset($this->objectTypeCount[$objectType['objectClass']])) {
                            $label = _($objectType['label']);
                            $result .= image($objectType['image'], null, $label).'&nbsp;'.$this->objectTypeCount[$objectType['objectClass']].'&nbsp;&nbsp;';
                        }
                    }

                    $result .= '</div></div>';
                }

                // Close list
                $result .= $switch ? "<input type='hidden' id='list_workaround'>" : '';

                // Add scroll positioner
                $result .= '<script type="text/javascript" language="javascript">';
                $result .= '$("t_nscrollbody").scrollTop= '.$this->scrollPosition.';';
                $result .= 'var box = $("t_nscrollbody").onscroll= function() {$("position_'.$this->pid.'").value= this.scrollTop;}';
                $result .= '</script>';
                break;
            default:
                /** Pagination inizialisation (uncomment for using)
                 *  get items per page
                 *  $prow = isset($_GET['items']) ? (int)$_GET['items'] : 10;

                 *  // start on page 1
                 *  $page = 1;

                 *  // get selected page
                 *  if (!isset($_GET['page'])) {
                 *      $selectedPage = 1;
                 *  } else {
                 *      $selectedPage = $_GET['page'];
                 *  }
                */

                /** if statement in foreachloop to filter results for pagination
                 * if (($row + 1) <= ($selectedPage * $prow) && ($row + 1) >= (($selectedPage - 1) * $prow))
                 */

                /** pagination function call
                 * $pagination = $this->setPaginator($prow, $page, $selectedPage);
                 */

                $result .= "  <div class='section scrollspy table-wrapper card-content-scroll' id='d_scrollbody'>
                                <table id='tree-table' class='highlight'>";
                $this->numColumns = count($this->colprops) + (($this->multiSelect | $this->singleSelect) ? 1 : 0);

                // Build list header
                $result .= '  <thead>
                                <tr>';
                if ($this->multiSelect || $this->singleSelect) {
                    $result .= '<th class="checkbox-column">';
                    if ($this->multiSelect) {
                        $result .= "  <label>
                                        <p>
                                            <input type='checkbox' id='select_all' name='select_all'
                                                title='"._('Select all')."' onClick='toggle_all_(\"listing_selected_[0-9]*$\",\"select_all\");' >
                                            <span></span>
                                        </p>
                                    </label>";
                    } else {
                        $result .= '&nbsp;';
                    }
                    $result .= '</th>';
                }
                foreach ($this->header as $header) {
                    $result .= $header;
                }
                $result .= '      </tr>
                            </thead>';

                // Build list body
                $result .= '  <tbody>';

                // No results? Just take an empty colspanned row
                if (count($this->entries) + count($this->departments) == 0) {
                    $result .= "  <tr>
                                    <td colspan='".$this->numColumns."'></td>
                                </tr>";
                }

                // Draw department browser if configured and we're not in sub mode
                $this->useSpan = false;
                if ($this->departmentBrowser && $this->filter->scope != 'sub') {
                    // Fill with department browser if configured this way
                    $departmentIterator = new departmentSortIterator($this->departments, $this->sortDirection[$this->sortColumn]);
                    foreach ($departmentIterator as $row => $entry) {
                        $rowResult = '<tr>';

                        // Render multi select if needed
                        if ($this->multiSelect || $this->singleSelect) {
                            $rowResult .= '<td></td>';
                        }

                        // Render defined department columns, fill the rest with some stuff
                        $rest = $this->numColumns - 1;
                        foreach ($this->xmlData['table']['department'] as $index => $conf) {
                            $colspan = 1;
                            if (isset($conf['span'])) {
                                $colspan = $conf['span'];
                                $this->useSpan = true;
                            }
                            $rowResult .= "<td colspan='$colspan'>".$this->renderCell($conf['value'], $entry, $row).'</td>';
                            $rest -= $colspan;
                        }

                        // Fill remaining cols with nothing
                        $last = $this->numColumns - $rest;
                        for ($i = 0; $i < $rest; ++$i) {
                            $rowResult .= '<td>&nbsp;</td>';
                        }
                        $rowResult .= '</tr>';

                        
                        // Apply label to objecttype icon?
                        if (preg_match("/<objectType:([^:]+):(.*)\/>/i", $rowResult, $matches)) {
                            $objectType = image("<i class='material-icons'>".$matches[1].'</i>', null, LDAP::fix(base64_decode($matches[2])));
                            $rowResult = preg_replace('/<objectType[^>]+>/', $objectType, $rowResult);
                        }
                        $result .= $rowResult;
                    }
                }

                // Fill with contents, sort as configured
                $ui = get_userinfo();

                foreach ($this->entries as $row => $entry) {
                    $trow = '';

                    // Render multi select if needed
                    if ($this->multiSelect) {
                        $trow .= "    <td>
                                            <label>
                                                <p>
                                                    <input type='checkbox' id='listing_selected_$row' name='listing_selected_$row'>
                                                    <span></span>
                                                </p>
                                            </label>
                                        </td>";
                    }

                    if ($this->singleSelect) {
                        $trow .= "    <td>
                                            <label>
                                                <p>
                                                    <input type='radio' id='listing_radio_selected_$row' name='listing_radio_selected' value='{$row}'>
                                                    <span></span>
                                                </p>
                                            </label>
                                        </td>";
                    }

                    $xmlLenprint = count($this->xmlData['table']['column']) - 1;
                    foreach ($this->xmlData['table']['column'] as $index => $conf) {
                        $renderedCell = $this->renderCell($conf['value'], $entry, $row);
                        if ($xmlLenprint == $index) {
                            $trow .= '<td class="last-column">'.$renderedCell.'</td>';
                        } else {
                            $trow .= '<td>'.$renderedCell.'</td>';
                        }

                        // Save rendered column
                        $sort = preg_replace('/.*>([^<]+)<.*$/', '$1', $renderedCell);
                        $sort = preg_replace('/&nbsp;/', '', $sort);
                        if (preg_match('/</', $sort)) {
                            $sort = '';
                        }
                        $this->entries[$row]["_sort$index"] = $sort;
                    }

                    // Save rendered entry
                    $this->entries[$row]['_rendered'] = $trow;
                }

                // Complete list by sorting entries for _sort$index and appending them to the output
                $entryIterator = new listingSortIterator($this->entries, $this->sortDirection[$this->sortColumn], '_sort'.$this->sortColumn, $this->sortType);
                foreach ($entryIterator as $row => $entry) {
                    // Apply label to objecttype icon?
                    if (preg_match("/<objectType:([^:]+):(.*)\/>/i", $entry['_rendered'], $matches)) {
                        if (preg_match("/<rowLabel:([a-z0-9_-]*)\/>/i", $entry['_rendered'], $m)) {
                            $objectType = image("<div class='entry-locked-td'><i class='material-icons'>".$matches[1].'['.$m[1].']'."</i></div>", null, LDAP::fix(base64_decode($matches[2])));
                        } else {
                            $objectType = image("<i class='material-icons'>".$matches[1].'</i>', null, LDAP::fix(base64_decode($matches[2])));
                        }
                        $entry['_rendered'] = preg_replace('/<objectType[^>]+>/', $objectType, $entry['_rendered']);
                        $entry['_rendered'] = preg_replace('/<rowLabel[^>]+>/', '', $entry['_rendered']);
                    }

                    // Apply custom class to row?
                    if (preg_match("/<rowClass:([a-z0-9_-]*)\/>/i", $entry['_rendered'], $matches)) {
                        $result .= "<tr class='".$matches[1]."'>\n";
                        $result .= preg_replace('/<rowClass[^>]+>/', '', $entry['_rendered']);
                    } else {
                        $result .= '<tr>';
                        $result .= $entry['_rendered'];
                    }

                    $result .= '</tr>';
                }

                // Close list body
                $result .= '         </tbody>
                                </table>
                            </div>';

                // Add the footer if requested
                if ($this->showFooter) {
                    $result .= '<div class="table-summary">';
                    foreach ($this->objectTypes as $objectType) {
                        if (isset($this->objectTypeCount[$objectType['label']])) {
                            $label = _($objectType['label']);
                            $result .= image("<i class='material-icons'>".$objectType['image'].'</i>', null, $label).'&nbsp;'.$this->objectTypeCount[$objectType['label']].'&nbsp;&nbsp;';
                        } elseif (isset($this->objectTypeCount[$objectType['objectClass']])) {
                            $label = _($objectType['label']);
                            $result .= image("<i class='material-icons'>".$objectType['image'].' </i>', null, $label).'&nbsp;'.$this->objectTypeCount[$objectType['objectClass']].'&nbsp;&nbsp;';
                        }
                    }

                    $result .= '</div>';
                }

                // Close list
                $result .= $switch ? "<input type='hidden' id='list_workaround'>" : '';
                break;
        }

        $smarty = get_smarty();

        $smarty->assign('FILTER', $this->filter->render());
        $smarty->assign('SIZELIMIT', print_sizelimit_warning());
        $smarty->assign('LIST', $result);

        // Assign navigation elements
        $nav = $this->renderNavigation();
        foreach ($nav as $key => $html) {
            $smarty->assign($key, $html);
        }

        // Assign action menu / base
        $smarty->assign('HEADLINE', $this->headline);
        $smarty->assign('ACTIONS', $this->renderActionMenu());
        $smarty->assign('BASE', $this->renderBase());

        // Assign separator
        $smarty->assign('SEPARATOR', "<img src='images/lists/seperator.png' alt='-' align='middle' height='16' width='1' class='center'>");

        // Assign summary
        $smarty->assign('HEADLINE', $this->headline);

        // Try to load template from plugin the folder first...
        $file = get_template_path($this->xmlData['definition']['template'], true);

        // ... if this fails, try to load the file from the theme folder.
        if (!file_exists($file)) {
            $file = get_template_path($this->xmlData['definition']['template']);
        }

        return $smarty->fetch($file);
    }

    // Pagination function excluded in standard
    public function setPaginator($prow, $page, $selectedPage)
    {
        // skip paginator if only one page is filled
        if (count($this->entries) / $prow > 1) {
            // set the paginator for current view
            $paginator = "  <div class='pagination-wrapper'>
                                <div class='page-items'>
                                    <p>Items per page</p>
                                    <select id='page-items' onchange='setItemCount()'>";
            foreach (array(5,10,25,50,75,100) as $index) {
                if ($index != $prow) {
                    $paginator .= "<option value=".$this->setPageLink($page, $index).">".$index."</option>";
                } else {
                    $paginator .= "<option value='".$index."' disabled selected>".$prow."</option>";
                }
            }

            $paginator .= "         </select>
                                </div>
                                <ul class='pagination valign-wrapper' id='pager'>";

            // first page
            $paginator .= $selectedPage > 1 ?
                            "<li><a href=".$this->setPageLink($page, $prow)."><i class='material-icons'>first_page</i></a></li>" :
                            "<li class='disabled'><a href='#!'><i class='material-icons'>first_page</i></a></li>";

            // next page
            $pageLink = "href='?plug=".$_GET['plug']."&amp;PID=".$this->pid."&amp;act=SORT_$index&amp;page=".($selectedPage - 1)."&amp;items=".$prow."'";
            $paginator .= $selectedPage > 1 ?
                            "<li><a href=".$this->setPageLink($selectedPage-1, $prow)."><i class='material-icons'>chevron_left</i></a></li>" :
                            "<li class='disabled'><a href='#!'><i class='material-icons'>chevron_left</i></a></li>";

            foreach ($this->entries as $row => $entry) {
                if (($row + 1) % $prow == 0) {
                    if (in_array($page, range($selectedPage - 1, $selectedPage + 1))) {
                        $paginator .= "<li class='".($selectedPage == $page ? 'active white-text' : '')."'><a href=".$this->setPageLink($page, $prow).">$page</a></li>";
                    }
                    if ($selectedPage == 1 && $page == 3) {
                        $paginator .= "<li class='".($selectedPage == $page ? 'active white-text' : '')."'><a href=".$this->setPageLink($page, $prow).">$page</a></li>";
                    }
                    ++$page;
                }
                $last = ($row + 1) % $prow != 0;
            }

            // append page if remaining entries not fill modulo of $prow
            if ($last && in_array($selectedPage, range($page -1, $page))) {
                $paginator .= "<li class='".($selectedPage == $page ? 'active white-text' : '')."'><a href=".$this->setPageLink($page, $prow).">$page</a></li>";
            }


            // next page
            $paginator .= $selectedPage < $page ?
                            "<li><a href=".$this->setPageLink(($selectedPage + 1), $prow)."><i class='material-icons'>chevron_right</i></a></li>" :
                            "<li class='disabled'><a href='#!'><i class='material-icons'>chevron_right</i></a></li>";

            // last page
            $paginator .= $selectedPage < $page ?
                            "<li><a href=".$this->setPageLink($page, $prow)."><i class='material-icons'>last_page</i></a></li>" :
                            "<li class='disabled'><a href='#!'><i class='material-icons'>last_page</i></a></li>";

            $paginator .= '     </ul>
                            </div>';
        }

        return $paginator;
    }

    public function setPageLink($page, $items)
    {
        return "?plug=".$_GET['plug']."&amp;PID=".$this->pid."&amp;act=SORT_$index&amp;page=$page&amp;items=$items";
    }

    public function update()
    {
        global $config;
        $ui = get_userinfo();

        // Take care of base selector
        if ($this->baseMode) {
            $this->baseSelector->update();

            // Check if a wrong base was supplied
            if (!$this->baseSelector->checkLastBaseUpdate()) {
                msg_dialog::display(_('Error'), msgPool::check_base(), ERROR_DIALOG);
            }
        }

        // Save base
        $refresh = false;
        if ($this->baseMode) {
            $this->base = $this->baseSelector->getBase();
            session::global_set('CurrentMainBase', $this->base);
            $refresh = true;
        }

        // Reset object counter / DN mapping
        $this->objectTypeCount = [];
        $this->objectDnMapping = [];

        // Do not do anything if this is not our PID
        if ($refresh || !(isset($_REQUEST['PID']) && $_REQUEST['PID'] != $this->pid)) {
            // Save position if set
            if (isset($_POST['position_'.$this->pid]) && is_numeric($_POST['position_'.$this->pid])) {
                $this->scrollPosition = get_post('position_'.$this->pid);
            }

            // Override the base if we got a message from the browser navigation
            if ($this->departmentBrowser && isset($_GET['act'])) {
                if (preg_match('/^department_([0-9]+)$/', validate($_GET['act']), $match)) {
                    if (isset($this->departments[$match[1]])) {
                        $this->base = $this->departments[$match[1]]['dn'];
                        if ($this->baseMode) {
                            $this->baseSelector->setBase($this->base);
                        }
                        session::global_set('CurrentMainBase', $this->base);
                    }
                }
            }

            // Filter POST with "act" attributes -> posted from action menu
            if (isset($_POST['exec_act']) && $_POST['act'] != '') {
                if (preg_match('/^export.*$/', $_POST['act']) && isset($this->exporter[$_POST['act']])) {
                    $exporter = $this->exporter[get_post('act')];
                    $userinfo = ', '._('created by').' '.$ui->cn.' - '.strftime('%A, %d. %B %Y, %H:%M:%S');
                    $entryIterator = new listingSortIterator($this->entries, $this->sortDirection[$this->sortColumn], '_sort'.$this->sortColumn, $this->sortType);
                    $sortedEntries = [];
                    foreach ($entryIterator as $entry) {
                        $sortedEntries[] = $entry;
                    }
                    $instance = new $exporter['class']($this->headline.$userinfo, $this->plainHeader, $sortedEntries, $this->exportColumns);
                    $type = call_user_func([$exporter['class'], 'getInfo']);
                    $type = $type[get_post('act')];
                    send_binary_content($instance->query(), $type['filename'], $type = $type['mime']);
                }
            }

            // Filter GET with "act" attributes
            if (isset($_GET['act'])) {
                $key = validate($_GET['act']);
                if (preg_match('/^SORT_([0-9]+)$/', $key, $match)) {
                    // Switch to new column or invert search order?
                    $column = $match[1];
                    if ($this->sortColumn != $column) {
                        $this->sortColumn = $column;
                    } else {
                        $this->sortDirection[$column] = !$this->sortDirection[$column];
                    }

                    // Allow header to update itself according to the new sort settings
                    $this->renderHeader();
                }
            }

            // Override base if we got signals from the navigation elements
            $action = '';
            foreach ($_POST as $key => $value) {
                if (preg_match('/^(ROOT|BACK|HOME)(_x)?$/', $key, $match)) {
                    $action = $match[1];
                    break;
                }
            }

            // Navigation handling
            if ($action == 'ROOT') {
                $deps = $ui->get_module_departments($this->categories);
                $this->base = $deps[0];
                $this->baseSelector->setBase($this->base);
                session::global_set('CurrentMainBase', $this->base);
            }
            if ($action == 'BACK') {
                $deps = $ui->get_module_departments($this->categories);
                $base = preg_replace('/^[^,]+,/', '', $this->base);
                if (in_array_ics($base, $deps)) {
                    $this->base = $base;
                    $this->baseSelector->setBase($this->base);
                    session::global_set('CurrentMainBase', $this->base);
                }
            }
            if ($action == 'HOME') {
                $ui = get_userinfo();
                $this->base = get_base_from_people($ui->dn);
                $this->baseSelector->setBase($this->base);
                session::global_set('CurrentMainBase', $this->base);
            }
        }

        // Reload departments
        if ($this->departmentBrowser) {
            $this->departments = $this->getDepartments();
        }

        // Update filter and refresh entries
        $this->filter->setBase($this->base);
        $this->entries = $this->filter->query();

        // Check entry acls
        if (!$this->noAclChecks) {
            foreach ($this->entries as $row => $entry) {
                $acl = '';
                $found = false;
                foreach ($this->aclToObjectClass as $category => $ocs) {
                    if (count(array_intersect($ocs, $entry['objectClass']))) {
                        $acl .= $ui->get_permissions($entry['dn'], $category, 0);
                        $found = true;
                    }
                }
                if (!preg_match('/r/', $acl) && $found) {
                    unset($this->entries[$row]);
                    continue;
                }
            }
        }

        // Fix filter if querie returns NULL
        if ($this->entries == null) {
            $this->entries = [];
        }
    }

    public function setBase($base)
    {
        $this->base = $base;
        if ($this->baseMode) {
            $this->baseSelector->setBase($this->base);
        }
    }

    public function getBase()
    {
        return $this->base;
    }

    public function parseLayout($layout)
    {
        $result = [];
        $layout = preg_replace("/^\|/", '', $layout);
        $layout = preg_replace("/\|$/", '', $layout);
        $cols = explode('|', $layout);

        foreach ($cols as $index => $config) {
            if ($config != '') {
                $res = '';
                $components = explode(';', $config);
                foreach ($components as $part) {
                    if (preg_match('/^r$/', $part)) {
                        $res .= 'text-align:right;';
                        continue;
                    }
                    if (preg_match('/^l$/', $part)) {
                        $res .= 'text-align:left;';
                        continue;
                    }
                    if (preg_match('/^c$/', $part)) {
                        $res .= 'text-align:center;';
                        continue;
                    }
                    if (preg_match('/^[0-9]+(|px|%)$/', $part)) {
                        $res .= "width:$part;min-width:$part;";
                        continue;
                    }
                }

                // Add minimum width for scalable columns
                if (!preg_match('/width:/', $res)) {
                    $res .= 'min-width:200px;';
                }

                $result[$index] = " style='$res'";
            } else {
                $result[$index] = " style='min-width:100px;'";
            }
        }

        // Save number of columns for later use
        $this->numColumns = count($cols);

        // Add no border to the last column
        $result[$this->numColumns - 1] = preg_replace("/'$/", "border-right:0;'", $result[$this->numColumns - 1]);

        return $result;
    }

    public function renderCell($data, $config, $row)
    {
        // Replace flat attributes in data string
        for ($i = 0; $i < $config['count']; ++$i) {
            $attr = $config[$i];
            $value = '';
            if (is_array($config[$attr])) {
                $value = $config[$attr][0];
            } else {
                $value = $config[$attr];
            }
            $data = preg_replace("/%\{$attr\}/", $value, $data);
        }

        // Watch out for filters and prepare to execute them
        $data = $this->processElementFilter($data, $config, $row);

        // Replace all non replaced %{...} instances because they
        // are non resolved attributes or filters
        $data = preg_replace('/%{[^}]+}/', '&nbsp;', $data);

        return $data;
    }

    public function renderBase()
    {
        if (!$this->baseMode) {
            return;
        }

        return $this->baseSelector->render();
    }

    public function processElementFilter($data, $config, $row)
    {
        preg_match_all("/%\{filter:([^(]+)\((.*)\)\}/", $data, $matches, PREG_SET_ORDER);

        foreach ($matches as $match) {
            $cl = '';
            $method = '';
            if (preg_match('/::/', $match[1])) {
                $cl = preg_replace('/::.*$/', '', $match[1]);
                $method = preg_replace('/^.*::/', '', $match[1]);
            } else {
                if (!isset($this->filters[$match[1]])) {
                    continue;
                }
                $cl = preg_replace('/::.*$/', '', $this->filters[$match[1]]);
                $method = preg_replace('/^.*::/', '', $this->filters[$match[1]]);
            }

            // Prepare params for function call
            preg_match_all('/"[^"]+"|[^,]+/', $match[2], $parts);
            foreach ($parts[0] as $param) {
                // Row is replaced by the row number
                if ($param == 'row') {
                    $params[] = $row;
                    continue;
                }

                // pid is replaced by the current PID
                if ($param == 'pid') {
                    $params[] = $this->pid;
                    continue;
                }

                // base is replaced by the current base
                if ($param == 'base') {
                    $params[] = $this->getBase();
                    continue;
                }

                // Fixie with "" is passed directly
                if (preg_match('/^".*"$/', $param)) {
                    $params[] = preg_replace('/"/', '', $param);
                    continue;
                }

                // Move dn if needed
                if ($param == 'dn') {
                    $params[] = LDAP::fix($config['dn']);
                    continue;
                }

                // LDAP variables get replaced by their objects
                for ($i = 0; $i < $config['count']; ++$i) {
                    if ($param == $config[$i]) {
                        $values = $config[$config[$i]];
                        if (is_array($values)) {
                            unset($values['count']);
                        }
                        $params[] = $values;
                        break;
                    }
                }
            }

            // Replace information
            if ($cl == 'listing') {
                // Non static call - seems to result in errors
                $data = @preg_replace('/'.preg_quote($match[0]).'/', call_user_func_array([$this, "$method"], $params), $data);
            } else {
                // Static call
                $data = preg_replace('/'.preg_quote($match[0]).'/', call_user_func_array([$cl, $method], $params), $data);
            }
        }

        return $data;
    }

    public function getObjectType($types, $classes)
    {
        // Walk thru types and see if there's something matching
        foreach ($types as $objectType) {
            $ocs = $objectType['objectClass'];
            if (!is_array($ocs)) {
                $ocs = [$ocs];
            }

            $found = true;
            foreach ($ocs as $oc) {
                if (preg_match('/^!(.*)$/', $oc, $match)) {
                    $oc = $match[1];
                    if (in_array_strict($oc, $classes)) {
                        $found = false;
                    }
                } else {
                    if (!in_array_strict($oc, $classes)) {
                        $found = false;
                    }
                }
            }

            if ($found) {
                return $objectType;
            }
        }

        return null;
    }

    public function filterObjectType($dn, $classes)
    {
        // Walk thru classes and return on first match
        $result = '&nbsp;';

        $objectType = $this->getObjectType($this->objectTypes, $classes);
        if ($objectType) {
            $this->objectDnMapping[$dn] = $objectType['objectClass'];
            $result = '<objectType:'.$objectType['image'].':'.base64_encode(LDAP::fix($dn)).'/>';
            if (!isset($this->objectTypeCount[$objectType['label']])) {
                $this->objectTypeCount[$objectType['label']] = 0;
            }
            ++$this->objectTypeCount[$objectType['label']];
        }

        return $result;
    }

    public function filterActions($dn, $row, $classes)
    {
        $ui = session::global_get('ui');

        // Do nothing if there's no menu defined
        if (!isset($this->xmlData['actiontriggers']['action'])) {
            return '&nbsp;';
        }

        // Go thru all actions
        $result = '';
        $actions = $this->xmlData['actiontriggers']['action'];

        // Ensure we've a valid actions array, if there is only one action in the actiontriggers col
        //  then we've to create a valid array here.
        if (isset($actions['name'])) {
            $actions = [$actions];
        }

        foreach ($actions as $action) {
            // Skip the entry completely if there's no permission to execute it
            if (!$this->hasActionPermission($action, $dn, $classes)) {
                switch ($this->theme) {
                    case 'classic':
                        $result .= image('images/empty.png');
                        break;

                    default:
                        $result .= image('');
                        break;
                }
                continue;
            }

            // Skip entry if the pseudo filter does not fit
            if (isset($action['filter']) && preg_match('/^[a-z0-9_]+!?=[a-z0-9_]+$/i', $action['filter'])) {
                list($fa, $fv) = explode('=', $action['filter']);
                if (preg_match('/^(.*)!$/', $fa, $m)) {
                    $fa = $m[1];
                    if (isset($this->entries[$row][$fa]) && $this->entries[$row][$fa][0] == $fv) {
                        switch ($this->theme) {
                            case 'classic':
                                $result .= image('images/empty.png');
                                break;

                            default:
                                $result .= image('');
                                break;
                        }
                        continue;
                    }
                } else {
                    if (!isset($this->entries[$row][$fa]) && !$this->entries[$row][$fa][0] == $fv) {
                        switch ($this->theme) {
                            case 'classic':
                                $result .= image('images/empty.png');
                                break;

                            default:
                                $result .= image('');
                                break;
                        }
                        continue;
                    }
                }
            }

            // If there's an objectclass definition and we don't have it
            // add an empty picture here.
            if (isset($action['objectclass'])) {
                $objectclass = $action['objectclass'];
                if (is_array($objectclass)) {
                    if (count(array_intersect($objectclass, $classes)) != count($objectclass)) {
                        switch ($this->theme) {
                            case 'classic':
                                $result .= image('images/empty.png');
                                break;

                            default:
                                $result .= image('');
                                break;
                        }
                        continue;
                    }
                } elseif (preg_match('/^!(.*)$/', $objectclass, $m)) {
                    $objectclass = $m[1];
                    if (in_array_strict($objectclass, $classes)) {
                        switch ($this->theme) {
                            case 'classic':
                                $result .= image('images/empty.png');
                                break;

                            default:
                                $result .= image('');
                                break;
                        }
                        continue;
                    }
                } elseif (is_string($objectclass)) {
                    if (!in_array_strict($objectclass, $classes)) {
                        switch ($this->theme) {
                            case 'classic':
                                $result .= image('images/empty.png');
                                break;

                            default:
                                $result .= image('');
                                break;
                        }
                        continue;
                    }
                }
            }

            // Render normal entries as usual
            if ($action['type'] == 'entry') {
                $label = $this->processElementFilter($action['label'], $this->entries[$row], $row);
                $image = $this->processElementFilter($action['image'], $this->entries[$row], $row);
                switch ($this->theme) {
                    case 'classic':
                        $result .= image($image, 'listing_'.$action['name']."_$row", $label);
                        break;

                    default:
                        $result .= image("<i class='material-icons input-icons'>".$image.'</i>', 'listing_'.$action['name']."_$row", $label);
                        break;
                }
            }

            // Handle special types
            if ($action['type'] == 'copypaste' || $action['type'] == 'snapshot') {
                $objectType = $this->getObjectType($this->objectTypes, $this->entries[$row]['objectClass']);
                $category = $class = null;
                if ($objectType) {
                    $category = $objectType['category'];
                    $class = $objectType['class'];
                }

                if ($action['type'] == 'copypaste') {
                    $copy = !isset($action['copy']) || $action['copy'] == 'true';
                    $cut = !isset($action['cut']) || $action['cut'] == 'true';
                    $result .= $this->renderCopyPasteActions($row, $this->entries[$row]['dn'], $category, $class, $copy, $cut);
                } else {
                    $result .= $this->renderSnapshotActions($row, $this->entries[$row]['dn'], $category, $class);
                }
            }
        }

        return $result;
    }

    public function filterDepartmentLink($row, $dn, $description)
    {
        $attr = $this->departments[$row]['sort-attribute'];
        $name = $this->departments[$row][$attr];
        if (is_array($name)) {
            $name = $name[0];
        }
        $result = sprintf('%s [%s]', $name, $description[0]);

        return "<a href='?plug=".$_GET['plug']."&amp;PID=$this->pid&amp;act=department_$row' title='$dn'>".set_post($result).'</a>';
    }

    public function filterLink()
    {
        $result = '&nbsp;';

        $row = func_get_arg(0);
        $pid = $this->pid;

        // Prepare title attribute
        $titleAttr = func_get_arg(1);
        if (is_array($titleAttr) && isset($titleAttr[0])) {
            $titleAttr = $titleAttr[0];
        }
        $titleAttr = LDAP::fix($titleAttr);

        $params = [func_get_arg(2)];

        // If we failed to collect enough parameters we shouldn't call sprintf!
        $found_args = false;

        // Collect sprintf params
        for ($i = 3; $i < func_num_args(); ++$i) {
            $found_args = true;
            $val = func_get_arg($i);
            if (is_array($val)) {
                $params[] = $val[0];
                continue;
            }
            $params[] = $val;
        }

        $result = '&nbsp;';

        if (!$found_args && is_array($params) && count($params) > 0 && strlen($params[0]) >= 0) {
            # Okay, we definitely have a string at $params[0] and *no* extra
            # parameters could be found.
            return $result;
        }

        $trans = sprintf(...$params);
        if ($trans != '') {
            return "<a href='?plug=".$_GET['plug']."&amp;PID=$pid&amp;act=listing_edit_$row' title='{$titleAttr}'>".set_post($trans).'</a>';
        }

        return $result;
    }

    public function renderNavigation()
    {
        global $config;

        $result = [];
        $enableBack = true;
        $enableRoot = true;
        $enableHome = true;

        $ui = get_userinfo();

        /* Check if base = first available base */
        $deps = $ui->get_module_departments($this->categories);

        if (!count($deps) || $deps[0] == $this->filter->base) {
            $enableBack = false;
            $enableRoot = false;
        }

        $listhead = '';

        /* Check if we are in users home  department */
        if (!count($deps) || $this->filter->base == get_base_from_people($ui->dn)) {
            $enableHome = false;
        }

        switch ($this->theme) {
            case 'classic':
                /* Draw root button */
                if ($enableRoot) {
                    $result['ROOT'] = image('images/lists/root.png', 'ROOT', _('Root'));
                } else {
                    $result['ROOT'] = image('images/lists/root-grey.png', null, _('Root'));
                }

                /* Draw back button */
                if ($enableBack) {
                    $result['BACK'] = image('images/lists/back.png', 'BACK', _('Go to preceding level'));
                } else {
                    $result['BACK'] = image('images/lists/back-grey.png', null, _('Go to preceding level'));
                }

                /* Draw home button */
                /* Draw home button */
                if ($enableHome) {
                    $result['HOME'] = image('images/lists/home.png', 'HOME', _('Go to current users level'));
                } else {
                    $result['HOME'] = image('images/lists/home-grey.png', null, _('Go to current users level'));
                }

                /* Draw reload button, this button is enabled everytime */
                $result['RELOAD'] = image('images/lists/reload.png', 'REFRESH', _('Reload list'));

                return $result;

            default:
                /* Draw root button */
                if ($enableRoot) {
                    $result['ROOT'] = image("<i class='material-icons input-icons md-24'>first_page</i>", 'ROOT', _('Root'));
                } else {
                    $result['ROOT'] = image("<i class='material-icons grey-text md-24'>first_page</i>", null, _('Root'));
                }

                /* Draw back button */
                if ($enableBack) {
                    $result['BACK'] = image("<i class='material-icons input-icons md-24'>chevron_left</i>", 'BACK', _('Go to preceding level'));
                } else {
                    $result['BACK'] = image("<i class='material-icons grey-text md-24'>chevron_left</i>", null, _('Go to preceding level'));
                }

                /* Draw home button */
                /* Draw home button */
                if ($enableHome) {
                    $result['HOME'] = image("<i class='material-icons input-icons md-24'>home</i>", 'HOME', _('Go to current users level'));
                } else {
                    $result['HOME'] = image("<i class='material-icons grey-text md-24'>home</i>", null, _('Go to current users level'));
                }

                /* Draw reload button, this button is enabled everytime */
                $result['RELOAD'] = image("<i class='material-icons input-icons'>refresh</i>", 'REFRESH', _('Reload list'));

                return $result;
        }
    }

    public function getAction()
    {
        // Do not do anything if this is not our PID, or there's even no PID available...
        if (!isset($_REQUEST['PID']) || $_REQUEST['PID'] != $this->pid) {
            return;
        }

        // Save position if set
        if (isset($_POST['position_'.$this->pid]) && is_numeric($_POST['position_'.$this->pid])) {
            $this->scrollPosition = get_post('position_'.$this->pid);
        }

        $result = ['targets' => [], 'action' => ''];

        // Filter GET with "act" attributes
        if (isset($_GET['act'])) {
            $key = validate($_GET['act']);
            $target = preg_replace('/^listing_[a-zA-Z_]+_([0-9]+)$/', '$1', $key);
            if (isset($this->entries[$target]['dn'])) {
                $result['action'] = preg_replace('/^listing_([a-zA-Z_]+)_[0-9]+$/', '$1', $key);
                $result['targets'][] = $this->entries[$target]['dn'];
            }

            // Drop targets if empty
            if (count($result['targets']) == 0) {
                unset($result['targets']);
            }

            return $result;
        }

        // Get single selection (radio box)
        if ($this->singleSelect && isset($_POST['listing_radio_selected'])) {
            $entry = get_post('listing_radio_selected');
            $result['targets'] = [$this->entries[$entry]['dn']];
        }

        // Filter POST with "listing_" attributes
        foreach ($_POST as $key => $prop) {
            $prop = get_post($key);

            // Capture selections
            if (preg_match('/^listing_selected_[0-9]+$/', $key)) {
                $target = preg_replace('/^listing_selected_([0-9]+)$/', '$1', $key);
                if (isset($this->entries[$target]['dn'])) {
                    $result['targets'][] = $this->entries[$target]['dn'];
                }
                continue;
            }

            // Capture action with target - this is a one shot
            if (preg_match('/^listing_[a-zA-Z_]+_[0-9]+(|_x)$/', $key)) {
                $target = preg_replace('/^listing_[a-zA-Z_]+_([0-9]+)(|_x)$/', '$1', $key);
                if (isset($this->entries[$target]['dn'])) {
                    $result['action'] = preg_replace('/^listing_([a-zA-Z_]+)_[0-9]+(|_x)$/', '$1', $key);
                    $result['targets'] = [$this->entries[$target]['dn']];
                }
                break;
            }

            // Capture action without target
            if (preg_match('/^listing_[a-zA-Z_]+(|_x)$/', $key)) {
                $result['action'] = preg_replace('/^listing_([a-zA-Z_]+)(|_x)$/', '$1', $key);
                continue;
            }
        }

        // Filter POST with "act" attributes -> posted from action menu
        if (isset($_POST['act']) && $_POST['act'] != '') {
            if (!preg_match('/^export.*$/', $_POST['act'])) {
                $result['action'] = get_post('act');
            }
        }

        // Drop targets if empty
        if (count($result['targets']) == 0) {
            unset($result['targets']);
        }

        return $result;
    }

    public function renderActionMenu()
    {
        static $level = 1;

        switch ($this->theme) {
            case 'classic':
                $result = "<input type='hidden' name='act' id='act' value=''><div style='display:none'><input type='submit' name='exec_act' id='exec_act' value=''></div>";

                // Don't send anything if the menu is not defined
                if (!isset($this->xmlData['actionmenu']['action'])) {
                    return $result;
                }

                // Array?
                if (isset($this->xmlData['actionmenu']['action']['type'])) {
                    $this->xmlData['actionmenu']['action'] = [$this->xmlData['actionmenu']['action']];
                }

                // Load shortcut
                $actions = &$this->xmlData['actionmenu']['action'];
                $result .= "<ul class='level1' id='root'><li><a href='#'>"._('Actions').'&nbsp;'.image('images/lists/sort-down.png').'</a>';

                // Build ul/li list
                $result .= $this->recurseActions($actions, $level);

                return "<div id='pulldown'>".$result.'</li></ul></div>';

            default:
                $result = " <input type='hidden' name='act' id='act' value=''>
                            <div style='display:none'>
                                <input type='submit' name='exec_act' id='exec_act' value=''>
                            </div>";

                // Don't send anything if the menu is not defined
                if (!isset($this->xmlData['actionmenu']['action'])) {
                    return $result;
                }


                // Array?
                if (isset($this->xmlData['actionmenu']['action']['type'])) {
                    $this->xmlData['actionmenu']['action'] = [$this->xmlData['actionmenu']['action']];
                }

                // Load shortcut
                $actions = &$this->xmlData['actionmenu']['action'];

                $result .= "  <a class='dropdown-menu' data-target='$level'><i class='material-icons md-24 right'>arrow_drop_down</i>"._('Actions')."</a>
                            <ul class='dropdown-content' id='$level'>";

                $result .= $this->recurseActions($actions, $level).'</ul></>';

                return $result;
        }
    }

    public function recurseActions($actions, $level)
    {
        global $class_mapping;

        $result = "";

        switch ($this->theme) {
            case 'classic':
                static $level = 2;
                $result = "<ul class='level$level'>";
                break;

            default:
                break;
        }

        $separator = '';
        foreach ($actions as $action) {

            // Skip the entry completely if there's no permission to execute it
            if (!$this->hasActionPermission($action, $this->filter->base)) {
                continue;
            }


            // Skip entry if there're missing dependencies
            if (isset($action['depends'])) {
                $deps = is_array($action['depends']) ? $action['depends'] : [$action['depends']];
                foreach ($deps as $clazz) {
                    if (!isset($class_mapping[$clazz])) {
                        continue 2;
                    }
                }
            }

            // Fill image if set
            $img = '';

            if (isset($action['image'])) {
                switch ($this->theme) {
                    case 'classic':
                        $img = image($action['image']).'&nbsp;';
                        break;

                    default:
                        $img = image("<i class='material-icons'>".$action['image']."</i>");
                        break;
                }
            }

            if ($action['type'] == 'separator') {
                switch ($this->theme) {
                    case 'classic':
                        $separator = "style='border-top:1px solid #AAA'";
                        break;

                    default:
                        $separator = "class='divider' tabiindex='-1'";
                        break;
                }
            }

            switch ($this->theme) {
                case 'classic':
                    // Dive into subs
                    if ($action['type'] == 'sub' && isset($action['action'])) {
                        ++$level;
                        if (isset($action['label'])) {
                            $result .= "<li$separator><a href='#'>$img"._($action['label']).'&nbsp;'.image('images/forward-arrow.png').'</a>';
                        }

                        // Ensure we've an array of actions, this enables sub menus with only one action.
                        if (isset($action['action']['type'])) {
                            $action['action'] = [$action['action']];
                        }

                        $result .= $this->recurseActions($action['action'], $level).'</li>';
                        --$level;
                        $separator = '';
                        continue 2;
                    }

                    // Render entry elseways
                    if (isset($action['label'])) {
                        $result .= "<li$separator><a href='#' onClick='\$(\"act\").value= \"".$action['name']."\";\$(\"exec_act\").click();'>$img"._($action['label']).'</a></li>';
                    }

                    // Check for special types
                    switch ($action['type']) {
                    case 'copypaste':
                        $cut = !isset($action['cut']) || $action['cut'] != 'false';
                        $copy = !isset($action['copy']) || $action['copy'] != 'false';
                        $result .= $this->renderCopyPasteMenu($separator, $copy, $cut);
                        break;

                    case 'snapshot':
                        $result .= $this->renderSnapshotMenu($separator);
                        break;

                    case 'exporter':
                        $result .= $this->renderExporterMenu($separator);
                        break;

                    case 'daemon':
                        $result .= $this->renderDaemonMenu($separator);
                        break;
                    }

                    $separator = '';
                    break;

                default:
                    // Dive into subs
                    if ($action['type'] == 'sub' && isset($action['action'])) {
                        ++$level;

                        $result .= "<li class='pointer_off'>
                                    <ul class='collection with-header'>";

                        if (isset($action['label'])) {
                            $result .= "<li class='collection-header pointer_off'><h3 id='head'>"._($action['label']).'</h3></li>';
                        } else {
                            $result .= "<li class='collection-header valign-wrapper'><i class='material-icons right'>"._($action['image']).'</i></li>';
                        }

                        // Ensure we've an array of actions, this enables sub menus with only one action.
                        if (isset($action['action']['type'])) {
                            $action['action'] = [$action['action']];
                        }

                        $result .= $this->recurseActions($action['action'], $level).'</ul>';

                        --$level;
                        $separator = '';
                        continue 2;
                    }

                    if (isset($action['label'])) {
                        if ($level < 1) {
                            $result .= "<ul class='collection' onClick='\$(\"act\").value= '".$action['name']."';\$(\"exec_act\").click();'>
                                            <li class='collection-item'>
                                                <a href='#'>
                                                    $img&nbsp;
                                                    "._($action['label']).'
                                                </a>
                                            </li>
                                        </ul>';
                        } else {
                            $result .= "<li class='collection-item' onClick='\$(\"act\").value= \"".$action['name']."\";\$(\"exec_act\").click();'>
                                            <a href='#'>
                                                $img
                                                "._($action['label']).'
                                            </a>
                                        </li>';
                        }
                    }

                    // Check for special types
                    switch ($action['type']) {
                    case 'copypaste':
                        $cut = !isset($action['cut']) || $action['cut'] != 'false';
                        $copy = !isset($action['copy']) || $action['copy'] != 'false';
                        $result .= $this->renderCopyPasteMenu($separator, $copy, $cut);
                        break;

                    case 'snapshot':
                        $result .= $this->renderSnapshotMenu($separator);
                        break;

                    case 'exporter':
                        $result .= $this->renderExporterMenu($separator);
                        break;

                    case 'daemon':
                        $result .= $this->renderDaemonMenu($separator);
                        break;
                    }
                    break;
            }
        }

        if (!$this->theme) {
            $result .= '</ul>';
        }

        return $result;
    }

    public function countValidEntries($action)
    {
        $count = 0;
        foreach ($action['action'] as $act) {
            if (isset($act['depends'])) {
                $deps = is_array($act['depends']) ? $act['depends'] : [$act['depends']];
                foreach ($deps as $clazz) {
                    if (isset($class_mapping[$clazz])) {
                        ++$count;
                    }
                }
            } else {
                ++$count;
            }
        }
        return $count;
    }

    public function hasActionPermission($action, $dn, $classes = null)
    {
        $ui = get_userinfo();

        if (isset($action['acl'])) {
            $acls = $action['acl'];
            if (!is_array($acls)) {
                $acls = [$acls];
            }

            // Every ACL has to pass
            foreach ($acls as $acl) {
                $module = $this->categories;
                $aclList = [];

                // Replace %acl if available
                if ($classes) {
                    $otype = $this->getObjectType($this->objectTypes, $classes);
                    $acl = str_replace('%acl', $otype['category'].'/'.$otype['class'], $acl);
                }

                // Split for category and plugins if needed
                // match for "[rw]" style entries
                if (preg_match('/^\[([rwcdm]+)\]$/', $acl, $match)) {
                    $aclList = [$match[1]];
                }

                // match for "users[rw]" style entries
                if (preg_match('/^([a-zA-Z0-9]+)\[([rwcdm]+)\]$/', $acl, $match)) {
                    $module = $match[1];
                    $aclList = [$match[2]];
                }

                // match for "users/user[rw]" style entries
                if (preg_match('/^([a-zA-Z0-9]+\/[a-zA-Z0-9]+)\[([rwcdm]+)\]$/', $acl, $match)) {
                    $module = $match[1];
                    $aclList = [$match[2]];
                }

                // match "users/user[userPassword:rw(,...)*]" style entries
                if (preg_match('/^([a-zA-Z0-9]+\/[a-zA-Z0-9]+)\[([a-zA-Z0-9]+:[rwcdm]+(,[a-zA-Z0-9]+:[rwcdm]+)*)\]$/', $acl, $match)) {
                    $module = $match[1];
                    $aclList = explode(',', $match[2]);
                }

                // Walk thru prepared ACL by using $module
                foreach ($aclList as $sAcl) {
                    $checkAcl = '';

                    // Category or detailed permission?
                    if (strpos($module, '/') !== false) {
                        if (preg_match('/([a-zA-Z0-9]+):([rwcdm]+)/', $sAcl, $m)) {
                            $checkAcl = $ui->get_permissions($dn, $module, $m[1]);
                            $sAcl = $m[2];
                        } else {
                            $checkAcl = $ui->get_permissions($dn, $module, '0');
                        }
                    } else {
                        $checkAcl = $ui->get_category_permissions($dn, $module);
                    }

                    // Split up remaining part of the acl and check if it we're
                    // allowed to do something...
                    $parts = str_split($sAcl);
                    foreach ($parts as $part) {
                        if (strpos($checkAcl, $part) === false) {
                            return false;
                        }
                    }
                }
            }
        }

        return true;
    }

    public function refreshBasesList()
    {
        global $config;
        $ui = get_userinfo();

        // Do some array munching to get it user friendly
        $ids = $config->idepartments;
        $d = $ui->get_module_departments($this->categories);
        $k_ids = array_keys($ids);
        $deps = array_intersect($d, $k_ids);

        // Fill internal bases list
        $this->bases = [];
        foreach ($k_ids as $department) {
            $this->bases[$department] = $ids[$department];
        }

        // Populate base selector if already present
        if ($this->baseSelector && $this->baseMode) {
            $this->baseSelector->setBases($this->bases);
            $this->baseSelector->update(true);
        }
    }

    public function getDepartments()
    {
        $departments = [];
        $ui = get_userinfo();

        // Get list of supported department types
        $types = departmentManagement::get_support_departments();

        // Load departments allowed by ACL
        $validDepartments = $ui->get_module_departments($this->categories);

        // Build filter and look in the LDAP for possible sub departments
        // of current base
        $filter = '(&(objectClass=gosaDepartment)(|';
        $attrs = ['description', 'objectClass'];
        foreach ($types as $name => $data) {
            $filter .= '(objectClass='.$data['OC'].')';
            $attrs[] = $data['ATTR'];
        }
        $filter .= '))';
        $res = get_list($filter, $this->categories, $this->base, $attrs, GL_NONE);

        // Analyze list of departments
        foreach ($res as $department) {
            if (!in_array_strict($department['dn'], $validDepartments)) {
                continue;
            }

            // Add the attribute where we use for sorting
            $oc = null;
            foreach (array_keys($types) as $type) {
                if (in_array_strict($type, $department['objectClass'])) {
                    $oc = $type;
                    break;
                }
            }
            $department['sort-attribute'] = $types[$oc]['ATTR'];

            // Move to the result list
            $departments[] = $department;
        }

        return $departments;
    }

    public function renderCopyPasteMenu($separator, $copy = true, $cut = true)
    {
        // We can only provide information if we've got a copypaste handler
        // instance
        if (!(isset($this->copyPasteHandler) && is_object($this->copyPasteHandler))) {
            return '';
        }

        // Presets
        $result = '';
        $read = $paste = false;
        $ui = get_userinfo();

        // Switch flags to on if there's at least one category which allows read/paste
        foreach ($this->categories as $category) {
            $read = $read || preg_match('/r/', $ui->get_category_permissions($this->base, $category));
            $paste = $paste || $ui->is_pasteable($this->base, $category) == 1;
        }

        // Draw entries that allow copy and cut
        if ($read) {
            // Copy entry
            if ($copy) {
                $result .= "<li$separator><a href='#' onClick='\$(\"act\").value= \"copy\";\$(\"exec_act\").click();'>".image('images/lists/copy.png').'&nbsp;'._('Copy').'</a></li>';
                $separator = '';
            }

            // Cut entry
            if ($cut) {
                $result .= "<li$separator><a href='#' onClick='\$(\"act\").value= \"cut\";\$(\"exec_act\").click();'>".image('images/lists/cut.png').'&nbsp;'._('Cut').'</a></li>';
                $separator = '';
            }
        }

        // Draw entries that allow pasting entries
        if ($paste) {
            if ($this->copyPasteHandler->entries_queued()) {
                $result .= "<li$separator><a href='#' onClick='\$(\"act\").value= \"paste\";\$(\"exec_act\").click();'>".image('images/lists/paste.png').'&nbsp;'._('Paste').'</a></li>';
            } else {
                $result .= "<li$separator><a href='#'>".image('images/lists/paste-grey.png').'&nbsp;'._('Paste').'</a></li>';
            }
        }

        return $result;
    }

    public function renderCopyPasteActions($row, $dn, $category, $class, $copy = true, $cut = true)
    {
        // We can only provide information if we've got a copypaste handler
        // instance
        if (!(isset($this->copyPasteHandler) && is_object($this->copyPasteHandler))) {
            return '';
        }

        // Presets
        $ui = get_userinfo();
        $result = '';

        // Render cut entries
        if ($cut) {
            if ($ui->is_cutable($dn, $category, $class)) {
                switch ($this->theme) {
                    case 'classic':
                        $result .= image('images/lists/cut.png', "listing_cut_$row", _('Cut this entry'));
                        break;

                    default:
                        $result .= image("<i class='material-icons>content_cut</i>'", "listing_cut_$row", _('Cut this entry'));
                        break;
                }
            } else {
                switch ($this->theme) {
                    case 'classic':
                        $result .= image('images/empty.png', "listing_cut_$row", _('Cut this entry'));
                        break;

                    default:
                        $result .= image('', "listing_cut_$row", _('Cut this entry'));
                        break;
                }
            }
        }

        // Render copy entries
        if ($copy) {
            if ($ui->is_copyable($dn, $category, $class)) {
                switch ($this->theme) {
                    case 'classic':
                        $result .= image('images/lists/copy.png', "listing_copy_$row", _('Copy this entry'));
                        break;

                    default:
                        $result .= image("<i class='material-icons>content_copy</i>", "listing_copy_$row", _('Copy this entry'));
                        break;
                }
            } else {
                switch ($this->theme) {
                    case 'classic':
                        $result .= image('images/empty.png');
                        break;

                    default:
                        $result .= image('');
                        break;
                }
            }
        }

        return $result;
    }

    public function renderSnapshotMenu($separator)
    {
        // We can only provide information if we've got a snapshot handler
        // instance
        if (!(isset($this->snapshotHandler) && is_object($this->snapshotHandler))) {
            return '';
        }

        // Presets
        $result = '';
        $ui = get_userinfo();

        if ($this->snapshotHandler->enabled() && $ui->allow_snapshot_restore($this->base, $this->categories)) {
            // Check if there is something to restore
            $restore = false;
            foreach ($this->snapshotHandler->getSnapshotBases() as $base) {
                $restore = $restore || count($this->snapshotHandler->getDeletedSnapshots($base)) > 0;
            }

            // Draw icons according to the restore flag
            if ($restore) {
                $result .= "<li$separator><a href='#' onClick='\$(\"act\").value= \"restore\";\$(\"exec_act\").click();'>".image('images/lists/restore.png').'&nbsp;'._('Restore snapshots').'</a></li>';
            } else {
                $result .= "<li$separator><a href='#'>".image('images/lists/restore-grey.png').'&nbsp;'._('Restore snapshots').'</a></li>';
            }
        }

        return $result;
    }

    public function renderExporterMenu($separator)
    {
        switch ($this->theme) {
            case 'classic':
                // Presets
                $result = '';

                // Draw entries
                $result .= "<li$separator><a href='#'>".image('images/lists/export.png').'&nbsp;'._('Export list').'&nbsp;'.image('images/forward-arrow.png')."</a><ul class='level3'>";

                // Export CVS as build in exporter
                foreach ($this->exporter as $action => $exporter) {
                    $result .= "<li><a href='#' onClick='\$(\"act\").value= \"$action\";\$(\"exec_act\").click();'>".image($exporter['image']).'&nbsp;'.$exporter['label'].'</a></li>';
                }

                // Finalize list
                $result .= '</ul></li>';
                break;

            default:
                // Presets
                $result = '';
                // Draw entries
                $result.="    <li class='pointer_off'>
                                <ul class='collection with-header'>
                                    <li class='collection-header pointer_off'><h3>"._("Export list")."</h3></li>";

                // Export CVS as build in exporter
                foreach ($this->exporter as $action => $exporter) {
                    $result .= "<li class='collection-item' onClick='\$(\"act\").value= \"$action\";\$(\"exec_act\").click();'>
                                    <a href='#'>
                                        <i class='material-icons'>".$exporter['image'].'</i>
                                        '.$exporter['label'].'
                                    </a>
                                </li>';
                }

                // Finalize list
                $result .= "</ul>
                        </li>";
                break;
        }
        return $result;
    }

    public function renderSnapshotActions($row, $dn, $category, $class, $copy = true, $cut = true)
    {
        // We can only provide information if we've got a snapshot handler
        // instance
        if (!(isset($this->snapshotHandler) && is_object($this->snapshotHandler))) {
            return '';
        }

        // Presets
        $result = '';
        $ui = get_userinfo();

        // Only act if enabled here
        if ($this->snapshotHandler->enabled()) {
            // Draw restore button
            if ($ui->allow_snapshot_restore($dn, $category)) {
                // Do we have snapshots for this dn?
                if ($this->snapshotHandler->hasSnapshots($dn)) {
                    switch ($this->theme) {
                        case 'classic':
                            $result .= image('images/lists/restore.png', "listing_restore_$row", _('Restore snapshot'));
                            break;

                        default:
                            $result .= image("<i class='material-icons'>restore</i>", "listing_restore_$row", _('Restore snapshot'));
                            break;
                    }
                } else {
                    $result .= image("<i class='material-icons'>restore</i>");
                }
            }

            // Draw snapshot button
            if ($ui->allow_snapshot_create($dn, $category)) {
                switch ($this->theme) {
                    case 'classic':
                        $result .= image('images/snapshot.png', "listing_snapshot_$row", _('Create new snapshot for this object'));
                        break;

                    default:
                        $result .= image("<i class='material-icons'>save</i>", "listing_snapshot_$row", _('Create new snapshot for this object'));
                        break;
                }
            } else {
                switch ($this->theme) {
                    case 'classic':
                        $result .= image('images/empty.png');
                        break;

                    default:
                        $result .= image("<i class='material-icons'>save</i>");
                        break;
                }
            }
        }

        return $result;
    }

    public function renderDaemonMenu($separator)
    {
        return '';
    }

    public function getEntry($dn)
    {
        foreach ($this->entries as $entry) {
            if (isset($entry['dn']) && strcasecmp($dn, $entry['dn']) == 0) {
                return $entry;
            }
        }

        return null;
    }

    public function getEntries()
    {
        return $this->entries;
    }

    public function getType($dn)
    {
        $dn = LDAP::fix($dn);
        if (isset($this->objectDnMapping[$dn])) {
            return $this->objectDnMapping[$dn];
        }

        return null;
    }
}
