class_simplePlugin.inc 70 KB
Newer Older
1 2 3
<?php
/*
  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
4 5

  Copyright (C) 2012-2019  FusionDirectory
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

  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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/

22 23 24 25 26
/*!
 * \file class_simplePlugin.inc
 * Source code for the class simplePlugin
 */

27 28 29
/*! \brief This class is made for easy plugin creation for editing LDAP attributes
 *
 */
30
class simplePlugin implements SimpleTab
31
{
32
  /*! \brief This attribute store all information about attributes */
33
  public $attributesInfo;
34

35 36 37 38
  /*! \brief This attribute store references toward attributes
   *
   * associative array that stores attributeLdapName => reference on object
   */
39
  public $attributesAccess = [];
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54

  /*!
    \brief Mark plugin as account

    Defines whether this plugin is defined as an account or not.
    This has consequences for the plugin to be saved from tab
    mode. If it is set to 'FALSE' the tab will call the delete
    function, else the save function. Should be set to 'TRUE' if
    the construtor detects a valid LDAP object.

    \sa simplePlugin::is_this_account()
   */
  public $is_account            = FALSE;
  public $initially_was_account = FALSE;
  public $ignore_account        = FALSE;
55

56 57 58
  public $acl_base      = '';
  public $acl_category  = '';

59 60 61 62 63 64 65 66 67 68 69 70 71
  /*! \brief dn of the opened object */
  public $dn      = '';

  /*! \brief original dn of the opened object */
  public $orig_dn = '';

  /*!
   * \brief Reference to parent object
   *
   * This variable is used when the plugin is included in tabs
   * and keeps reference to the tab class. Communication to other
   * tabs is possible by 'name'. So the 'fax' plugin can ask the
   * 'userinfo' plugin for the fax number.
72
   *
73
   * \sa simpleTabs
74
   */
75
  public $parent = NULL;
76

77 78 79 80 81 82
  /*!
    \brief Mark plugin as template

    Defines whether we are editing a template or a normal object.
    Has consequences on the way execute() shows the formular and how
    save() puts the data to LDAP.
83
   */
84 85
  public $is_template    = FALSE;

86 87 88
  /*!
    \brief Represent temporary LDAP data

89
    This should only be used internally.
90
   */
91
  public $attrs = [];
92 93

  /*! \brief The objectClasses set by this tab */
94
  protected $objectclasses = [];
95 96

  /*! \brief The state of the attributes when we opened the object */
97
  protected $saved_attributes = [];
98

99 100 101 102
  /*! \brief Do we want a header allowing to able/disable this plugin */
  protected $displayHeader = FALSE;

  /*! \brief Is this plugin the main tab, the one that handle the object itself */
103 104 105 106
  protected $mainTab = FALSE;

  protected $header = "";

107 108
  protected $templatePath;

109 110
  protected $dialog = FALSE;

111 112 113 114
  /*! \brief Are we executed in a edit-mode environment? (this is FALSE if we're called from management, TRUE if we're called from a main.inc)
   */
  protected $needEditMode = FALSE;

115
  /*! \brief Attributes that needs to be initialized before the others */
116
  protected $preInitAttributes = [];
117

118 119 120 121 122
  /*! \brief FALSE to disable inheritance. Array like array ('objectClass' => 'attribute') to specify oc of the groups it might be inherited from
   */
  protected $inheritance      = FALSE;
  protected $member_of_group  = FALSE;
  protected $editing_group    = NULL;
123
  protected $group_attrs      = [];
124

125 126 127
  /*! \brief Used when the entry is opened as "readonly" due to locks */
  protected $read_only = FALSE;

128 129 130
  /*! \brief Last LDAP error (used by logging calls from post_* methods) */
  protected $ldap_error;

131 132 133 134 135 136 137 138 139
  /*!
   * \brief Object entry CSN
   *
   * If an entry was edited while we have edited the entry too,
   * an error message will be shown.
   * To configure this check correctly read the FAQ.
   */
  protected $entryCSN = '';

Côme Chilliet's avatar
Côme Chilliet committed
140 141
  private $hadSubobjects = FALSE;

142 143 144 145
  /*! \brief constructor
   *
   *  \param string $dn The dn of this instance
   *  \param Object $object An object to copy values from
146 147
   *  \param Object $parent A parent instance, usually a simpleTabs instance.
   *  \param boolean $mainTab Whether or not this is the main tab
148 149 150
   *  \param array $attributesInfo An attributesInfo array, if NULL, getAttributesInfo will be used.
   *
   */
151
  function __construct (string $dn = NULL, $object = NULL, $parent = NULL, bool $mainTab = FALSE, array $attributesInfo = NULL)
152
  {
153
    global $config;
154

155
    $this->dn       = $dn;
156 157 158
    $this->parent   = $parent;
    $this->mainTab  = $mainTab;

159 160 161 162
    try {
      $plInfo = pluglist::pluginInfos(get_class($this));
    } catch (UnknownClassException $e) {
      /* May happen in special cases like setup */
163
      $plInfo = [];
164
    }
165

166
    if (empty($this->objectclasses) && isset($plInfo['plObjectClass'])) {
167 168 169
      $this->objectclasses = $plInfo['plObjectClass'];
    }

170 171 172 173 174 175 176 177
    if ($attributesInfo === NULL) {
      $attributesInfo = $this->getAttributesInfo();
    }
    if (!$this->displayHeader) {
      // If we don't display the header to activate/deactive the plugin, that means it's always activated
      $this->ignore_account = TRUE;
    }

178
    $this->attributesInfo = [];
179
    foreach ($attributesInfo as $section => $sectionInfo) {
180
      $attrs = [];
181 182 183 184 185 186 187 188 189 190 191 192
      foreach ($sectionInfo['attrs'] as $attr) {
        $name = $attr->getLdapName();
        if (isset($attrs[$name])) {
          // We check that there is no duplicated attribute name
          trigger_error("Duplicated attribute LDAP name '$name' in a simplePlugin subclass");
        }
        // We make so that attribute have their LDAP name as key
        // That allow the plugin to use $this->attributesInfo[$sectionName]['attrs'][$myLdapName] to retreive the attribute info.
        $attrs[$name] = $attr;
      }
      $sectionInfo['attrs']           = $attrs;
      $this->attributesInfo[$section] = $sectionInfo;
193 194 195 196 197 198 199 200
      foreach ($this->attributesInfo[$section]['attrs'] as $name => $attr) {
        if (isset($this->attributesAccess[$name])) {
          // We check that there is no duplicated attribute name
          trigger_error("Duplicated attribute LDAP name '$name' in a simplePlugin subclass");
        }
        $this->attributesAccess[$name] =& $this->attributesInfo[$section]['attrs'][$name];
        unset($this->$name);
      }
201
    }
202

203 204
    /* Ensure that we've a valid acl_category set */
    if (empty($this->acl_category)) {
205 206
      if (isset($plInfo['plCategory'])) {
        $c = key($plInfo['plCategory']);
207
        if (is_numeric($c)) {
208
          $c = $plInfo['plCategory'][0];
209 210 211 212 213 214 215 216
        }
        $this->acl_category = $c.'/';
      }
    }

    /* Handle read only */
    if ($this->dn != 'new') {
      /* Check if this entry was opened in read only mode */
217 218 219 220
      if (isset($_POST['open_readonly']) && session::global_is_set('LOCK_CACHE')) {
        $cache = session::get('LOCK_CACHE');
        if (isset($cache['READ_ONLY'][$this->dn])) {
          $this->read_only = TRUE;
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
        }
      }

      /* Save current dn as acl_base */
      $this->acl_base = $this->dn;
    }

    /* Load LDAP data */
    if (($this->dn != 'new' && $this->dn !== NULL) || ($object !== NULL)) {
      /* Load data to 'attrs' */
      if ($object !== NULL) {
        /* From object */
        $this->attrs = $object->attrs;
        if (isset($object->is_template)) {
          $this->setTemplate($object->is_template);
        }
      } else {
        /* From LDAP */
        $ldap = $config->get_ldap_link();
        $ldap->cat($this->dn);
        $this->attrs = $ldap->fetch();
        if (empty($this->attrs)) {
          throw new NonExistingLdapNodeException('Could not open dn '.$this->dn);
        }
245 246 247 248 249
        if ($this->mainTab) {
          /* Make sure that initially_was_account is TRUE if we loaded an LDAP node,
           *  even if it’s missing an objectClass */
          $this->is_account = TRUE;
        }
250 251 252
      }

      /* Set the template flag according to the existence of objectClass fdTemplate */
253
      if (isset($this->attrs['objectClass']) && in_array_ics('fdTemplate', $this->attrs['objectClass'])) {
254
        @DEBUG(DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, 'found', 'Template check');
255 256
        $this->setTemplate(TRUE);
        $this->templateLoadAttrs($this->attrs);
257 258 259 260 261
      }

      /* Is Account? */
      if ($this->is_this_account($this->attrs)) {
        $this->is_account = TRUE;
262
        @DEBUG(DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, get_class($this), 'Tab active');
263 264 265
      }
    }

266 267
    if (is_array($this->inheritance)) {
      /* Check group membership */
268 269
      $ldap = $config->get_ldap_link();
      $ldap->cd($config->current['BASE']);
270 271
      foreach ($this->inheritance as $oc => $at) {
        if ($this->mainTab) {
272
          $filter = '(&(objectClass='.$oc.')('.$at.'='.ldap_escape_f($this->dn).'))';
273
        } else {
274
          $filter = '(&(objectClass='.$oc.')'.static::getLdapFilter().'('.$at.'='.ldap_escape_f($this->dn).'))';
275 276 277 278 279 280 281 282 283 284
        }
        $ldap->search($filter, $this->attributes);
        if ($ldap->count() == 1) {
          $this->member_of_group = TRUE;
          $attrs = $ldap->fetch();
          $this->group_attrs = $attrs;
          break;
        }
      }
    }
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302

    /* Save initial account state */
    $this->initially_was_account = $this->is_account;

    $this->loadAttributes();

    $this->prepareSavedAttributes();

    $this->orig_dn = $dn;

    if ($this->mainTab) {
      $this->is_account = TRUE;
      $this->entryCSN = getEntryCSN($this->dn);
    }

    if (!isset($this->templatePath)) {
      $this->templatePath = get_template_path('simpleplugin.tpl');
    }
303 304
  }

305
  protected function loadAttributes ()
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
  {
    // We load attributes values
    // First the one flagged as preInit
    foreach ($this->preInitAttributes as $attr) {
      $this->attributesAccess[$attr]->setParent($this);
      $this->attributesAccess[$attr]->loadValue($this->attrs);
    }
    // Then the others
    foreach ($this->attributesInfo as &$sectionInfo) {
      foreach ($sectionInfo['attrs'] as $name => &$attr) {
        if (in_array($name, $this->preInitAttributes)) {
          /* skip the preInit ones */
          continue;
        }
        $attr->setParent($this);
        $attr->loadValue($this->attrs);
      }
      unset($attr);
    }
    unset($sectionInfo);
  }

328
  function is_this_account ($attrs)
329
  {
330 331 332 333
    $result = static::isAccount($attrs);
    if ($result === NULL) {
      if (!empty($this->objectclasses)) {
        trigger_error('Deprecated fallback was used for '.get_called_class().'::is_this_account');
334
      }
335 336 337 338 339
      $found = TRUE;
      foreach ($this->objectclasses as $obj) {
        if (preg_match('/^top$/i', $obj)) {
          continue;
        }
340
        if (!isset($attrs['objectClass']) || !in_array_ics($obj, $attrs['objectClass'])) {
341 342 343
          $found = FALSE;
          break;
        }
344
      }
345
      return $found;
346
    }
347
    return $result;
348 349
  }

350
  function setTemplate (bool $bool)
351
  {
352
    $this->is_template = $bool;
353 354 355
    if ($this->is_template && $this->mainTab) {
      /* Unshift special section for template infos */
      $this->attributesInfo = array_merge(
356 357 358
        [
          '_template' => [
            'class' => ['fullwidth'],
359
            'name'  => _('Template settings'),
360
            'attrs' => [
361 362
              '_template_cn' => new StringAttribute(
                _('Template name'), _('This is the name of the template'),
363 364
                '_template_cn', TRUE,
                '', 'template_cn'
365
              )
366 367 368 369
            ]
          ],
          '_template_dummy' => [
            'class' => ['invisible'],
370
            'name'  => '_template_dummy',
371 372 373
            'attrs' => []
          ]
        ],
374 375 376 377 378
        $this->attributesInfo
      );
      $this->attributesAccess['_template_cn'] =& $this->attributesInfo['_template']['attrs']['_template_cn'];
      $this->attributesAccess['_template_cn']->setInLdap(FALSE);
      $this->attributesAccess['_template_cn']->setValue($this->_template_cn);
379
      $this->attributesAccess['_template_cn']->setParent($this);
380 381 382 383
      unset($this->_template_cn);
    }
  }

384
  protected function templateLoadAttrs (array $template_attrs)
385 386 387 388 389 390 391
  {
    if ($this->mainTab) {
      $this->_template_cn = $template_attrs['cn'][0];
    }
    $this->attrs = templateHandling::fieldsFromLDAP($template_attrs);
  }

392
  protected function templateSaveAttrs ()
393 394 395 396 397 398 399 400 401
  {
    global $config;
    $ldap = $config->get_ldap_link();
    $ldap->cat($this->dn);
    $template_attrs = $ldap->fetch();
    if (!$template_attrs) {
      if (!$this->mainTab) {
        trigger_error('It seems main tab has not been saved.');
      }
402 403 404 405
      $template_attrs = [
        'objectClass'     => ['fdTemplate'],
        'fdTemplateField' => []
      ];
406 407 408 409 410 411 412 413
    }
    $template_attrs = templateHandling::fieldsToLDAP($template_attrs, $this->attrs);
    if ($this->mainTab) {
      $template_attrs['cn'] = $this->_template_cn;
    }
    return $template_attrs;
  }

414 415 416 417
  /*! \brief This function returns an LDAP filter for this plugin object classes
   */
  function getObjectClassFilter ()
  {
418 419
    trigger_error('Deprecated');
    return static::getLdapFilter();
420 421
  }

422
  /*! \brief This function allows to use the syntax $plugin->attributeName to get attributes values
423
   *
424 425 426
   * It calls the getValue method on the concerned attribute
   * It also adds the $plugin->attribtues syntax to get attributes list
   */
427
  public function __get ($name)
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
  {
    if ($name == 'attributes') {
      $plugin = $this;
      return array_filter(array_keys($this->attributesAccess),
        function ($a) use ($plugin)
        {
          return $plugin->attributesAccess[$a]->isInLdap();
        }
      );
    } elseif (isset($this->attributesAccess[$name])) {
      return $this->attributesAccess[$name]->getValue();
    } else {
      /* Calling default behaviour */
      return $this->$name;
    }
  }

  /*! \brief This function allows to use the syntax $plugin->attributeName to set attributes values

    It calls the setValue method on the concerned attribute
   */
449
  public function __set ($name, $value)
450
  {
451 452 453
    if ($name == 'attributes') {
      trigger_error('Tried to set obsolete attribute "attributes" (it is now dynamic)');
    } elseif (isset($this->attributesAccess[$name])) {
454 455 456 457 458 459 460 461 462 463 464
      $this->attributesAccess[$name]->setValue($value);
    } else {
      /* Calling default behaviour */
      $this->$name = $value;
    }
  }

  /*! \brief This function allows to use the syntax isset($plugin->attributeName)

    It returns FALSE if the attribute has an empty value.
   */
465
  public function __isset ($name)
466 467 468 469 470
  {
    if ($name == 'attributes') {
      return TRUE;
    }
    return isset($this->attributesAccess[$name]);
471 472
  }

473 474
  /*! \brief This function returns the dn this object should have
   */
475
  public function compute_dn (): string
476
  {
477
    global $config;
478
    if (!$this->mainTab) {
479 480
      msg_dialog::display(_('Fatal error'), _('Only main tab can compute dn'), FATAL_ERROR_DIALOG);
      exit;
481 482
    }
    if (!isset($this->parent) || !($this->parent instanceof simpleTabs)) {
483 484 485 486 487 488 489 490 491
      msg_dialog::display(
        _('Fatal error'),
        sprintf(
          _('Could not compute dn: no parent tab class for "%s"'),
          get_class($this)
        ),
        FATAL_ERROR_DIALOG
      );
      exit;
492 493 494
    }
    $infos = $this->parent->objectInfos();
    if ($infos === FALSE) {
495 496 497
      msg_dialog::display(
        _('Fatal error'),
        sprintf(
498
          _('Could not compute dn: could not find objectType infos from tab class "%s"'),
499 500 501 502 503
          get_class($this->parent)
        ),
        FATAL_ERROR_DIALOG
      );
      exit;
504 505 506
    }
    $attr = $infos['mainAttr'];
    $ou   = $infos['ou'];
507 508 509 510 511
    if (isset($this->base)) {
      $base = $this->base;
    } else {
      $base = $config->current['BASE'];
    }
512
    if ($this->is_template) {
513
      return 'cn='.ldap_escape_dn($this->_template_cn).',ou=templates,'.$ou.$base;
514
    }
515
    return $attr.'='.ldap_escape_dn($this->attributesAccess[$attr]->computeLdapValue()).','.$ou.$base;
516 517
  }

518
  protected function addAttribute (string $section, Attribute $attr)
519 520 521 522 523 524 525 526
  {
    $name = $attr->getLdapName();
    $this->attributesInfo[$section]['attrs'][$name] = $attr;
    $this->attributesAccess[$name] =& $this->attributesInfo[$section]['attrs'][$name];
    $this->attributesAccess[$name]->setParent($this);
    unset($this->$name);
  }

527
  protected function removeAttribute (string $section, string $id)
528 529 530 531 532
  {
    unset($this->attributesInfo[$section]['attrs'][$id]);
    unset($this->attributesAccess[$id]);
  }

533 534 535 536 537 538 539 540 541
  /*!
   * \brief Returns a list of all available departments for this object.
   *
   * If this object is new, all departments we are allowed to create a new object in are returned.
   * If this is an existing object, return all deps we are allowed to move this object to.
   * Used by BaseSelectorAttribute
   *
   * \return array [dn] => "..name"  // All deps. we are allowed to act on.
  */
542
  function get_allowed_bases (): array
543 544
  {
    global $config;
545
    $deps = [];
546 547 548

    /* Is this a new object ? Or just an edited existing object */
    foreach ($config->idepartments as $dn => $name) {
549 550 551 552
      if (
          (!$this->initially_was_account && $this->acl_is_createable($dn)) ||
          ($this->initially_was_account && $this->acl_is_moveable($dn))
        ) {
553 554 555 556 557 558 559 560 561 562 563 564 565
        $deps[$dn] = $name;
      }
    }

    /* Add current base */
    if (isset($this->base) && isset($config->idepartments[$this->base])) {
      $deps[$this->base] = $config->idepartments[$this->base];
    } elseif (strtolower($this->dn) != strtolower($config->current['BASE'])) {
      trigger_error('Cannot return list of departments, no default base found in class '.get_class($this).'. (base is "'.$this->base.'")');
    }
    return $deps;
  }

566 567 568 569 570
  /*!
   * \brief Set acl base
   *
   * \param string $base
   */
571
  function set_acl_base (string $base)
572 573 574 575 576 577 578 579 580
  {
    $this->acl_base = $base;
  }

  /*!
   * \brief Set acl category
   *
   * \param string $category
   */
581
  function set_acl_category (string $category)
582 583 584 585
  {
    $this->acl_category = "$category/";
  }

586 587 588 589 590 591
  /*!
    * \brief Move ldap entries from one place to another
    *
    * \param  string  $src_dn the source DN.
    *
    * \param  string  $dst_dn the destination DN.
592 593
    *
    * \return TRUE on success, error string on failure
594
    */
595
  function move (string $src_dn, string $dst_dn)
596 597 598 599 600 601 602 603 604 605 606 607 608 609
  {
    global $config, $ui;

    /* Do not move if only case has changed */
    if (strtolower($src_dn) == strtolower($dst_dn)) {
      return TRUE;
    }

    /* Try to move with ldap routines */
    $ldap = $config->get_ldap_link();
    $ldap->cd($config->current['BASE']);
    $ldap->create_missing_trees(preg_replace('/^[^,]+,/', '', $dst_dn));
    if (!$ldap->rename_dn($src_dn, $dst_dn)) {
      logging::log('debug', 'Ldap Protocol v3 implementation error, ldap_rename failed.',
610
        "FROM: $src_dn  -- TO: $dst_dn", [], $ldap->get_error());
611 612
      @DEBUG(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, "Rename failed FROM: $src_dn  -- TO:  $dst_dn",
        'Ldap Protocol v3 implementation error. Error:'.$ldap->get_error());
613
      return $ldap->get_error();
614 615 616 617 618
    }

    /* Update userinfo if necessary */
    if (preg_match('/'.preg_quote($src_dn, '/').'$/i', $ui->dn)) {
      $ui_dn = preg_replace('/'.preg_quote($src_dn, '/').'$/i', $dst_dn, $ui->dn);
619
      logging::log('view', 'acl/'.get_class($this), $this->dn, [], 'Updated userinfo dn from "'.$ui->dn.'" to "'.$ui_dn.'"');
620 621 622 623 624
      $ui->dn = $ui_dn;
    }

    /* Check if departments were moved. If so, force the reload of config->departments */
    $ldap->cd($dst_dn);
625
    $ldap->search('(objectClass=gosaDepartment)', ['dn']);
626 627 628 629 630 631 632 633 634 635
    if ($ldap->count()) {
      $config->get_departments();
      $config->make_idepartments();
      $ui->reset_acl_cache();
    }

    $this->handleForeignKeys($src_dn, $dst_dn);
    return TRUE;
  }

636
  function getRequiredAttributes (): array
637
  {
638
    $tmp = [];
639 640 641 642 643 644 645 646
    foreach ($this->attributesAccess as $attr) {
      if ($attr->isRequired()) {
        $tmp[] = $attr->getLdapName();
      }
    }
    return $tmp;
  }

647 648 649 650 651 652 653
  function editing_group ()
  {
    if ($this->editing_group == NULL) {
      if (isset($this->parent)) {
        $this->editing_group = (get_class($this->parent->getBaseObject()) == 'ogroup');
      } else {
        return NULL;
654 655
      }
    }
656
    return $this->editing_group;
657 658
  }

659
  /*! \brief Indicates if this object is opened as read-only (because of locks) */
660
  function readOnly ()
661 662 663 664
  {
    return $this->read_only;
  }

665 666
  /*! \brief This function display the plugin and return the html code
   */
667
  function execute (): string
668
  {
669
    @DEBUG(DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, $this->dn, "execute");
670 671

    /* Reset Lock message POST/GET check array, to prevent preg_match errors */
672 673 674 675
    session::set('LOCK_VARS_TO_USE', []);
    session::set('LOCK_VARS_USED_GET', []);
    session::set('LOCK_VARS_USED_POST', []);
    session::set('LOCK_VARS_USED_REQUEST', []);
676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693

    $this->displayPlugin  = TRUE;
    $this->header         = "";

    if (is_object($this->dialog)) {
      $dialogResult = $this->dialog->execute();
      if ($dialogResult === FALSE) {
        $this->closeDialog();
      } else {
        $this->header         = $dialogResult;
        $this->displayPlugin  = FALSE;
        return $this->header;
      }
    }

    if ($this->displayHeader) {
      /* Show tab dialog headers */
      if ($this->parent !== NULL) {
694 695 696 697 698 699 700 701 702
        list($disabled, $buttonText, $text) = $this->getDisplayHeaderInfos();
        $this->header = $this->show_header(
          $buttonText,
          $text,
          $this->is_account,
          $disabled,
          get_class($this).'_modify_state'
        );
        if (!$this->is_account) {
703
          $this->displayPlugin = FALSE;
704
          return $this->header.$this->inheritanceDisplay();
705 706
        }
      } elseif (!$this->is_account) {
707
        $plInfo = pluglist::pluginInfos(get_class($this));
708
        $this->header = '<img alt="'._('Error').'" src="geticon.php?context=status&amp;icon=dialog-error&amp;size=16" align="middle"/>&nbsp;<b>'.
709
                        msgPool::noValidExtension($plInfo['plShortName'])."</b>";
710
        $this->displayPlugin = FALSE;
711
        return $this->header.$this->inheritanceDisplay();
712 713 714 715 716
      }
    }

    $smarty = get_smarty();

717 718 719 720 721 722 723 724 725 726 727 728
    $this->renderAttributes(FALSE);
    $smarty->assign("hiddenPostedInput", get_class($this)."_posted");
    if (isset($this->focusedField)) {
      $smarty->assign("focusedField", $this->focusedField);
      unset($this->focusedField);
    } else {
      $smarty->assign("focusedField", key($this->attributesAccess));
    }

    return $this->header.$smarty->fetch($this->templatePath);
  }

729
  public function getDisplayHeaderInfos (): array
730 731 732 733
  {
    $plInfo   = pluglist::pluginInfos(get_class($this));
    $disabled = $this->acl_skip_write();
    if ($this->is_account) {
734
      $depends = [];
735 736 737 738 739 740 741 742 743 744 745 746 747
      if (isset($plInfo['plDepending'])) {
        foreach ($plInfo['plDepending'] as $plugin) {
          if (isset($this->parent->by_object[$plugin]) &&
              $this->parent->by_object[$plugin]->is_account) {
            $disabled       = TRUE;
            $dependPlInfos  = pluglist::pluginInfos($plugin);
            $depends[]      = $dependPlInfos['plShortName'];
          }
        }
      }
      $buttonText = msgPool::removeFeaturesButton($plInfo['plShortName']);
      $text       = msgPool::featuresEnabled($plInfo['plShortName'], $depends);
    } else {
748 749
      $depends    = [];
      $conflicts  = [];
750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772
      if (isset($plInfo['plDepends'])) {
        foreach ($plInfo['plDepends'] as $plugin) {
          if (isset($this->parent->by_object[$plugin]) &&
              !$this->parent->by_object[$plugin]->is_account) {
            $disabled   = TRUE;
            $dependPlInfos  = pluglist::pluginInfos($plugin);
            $depends[]      = $dependPlInfos['plShortName'];
          }
        }
      }
      if (isset($plInfo['plConflicts'])) {
        foreach ($plInfo['plConflicts'] as $plugin) {
          if (isset($this->parent->by_object[$plugin]) &&
              $this->parent->by_object[$plugin]->is_account) {
            $disabled   = TRUE;
            $conflictPlInfos  = pluglist::pluginInfos($plugin);
            $conflicts[]      = $conflictPlInfos['plShortName'];
          }
        }
      }
      $buttonText = msgPool::addFeaturesButton($plInfo['plShortName']);
      $text       = msgPool::featuresDisabled($plInfo['plShortName'], $depends, $conflicts);
    }
773
    return [$disabled,$buttonText,$text];
774 775
  }

776 777 778 779 780 781 782
  /*!
   * \brief Show header message for tab dialogs
   *
   * \param string $button_text The button text
   *
   * \param string $text The text
   *
783
   * \param boolean $plugin_enabled Is the plugin/tab activated
784
   *
785 786 787
   * \param boolean $button_disabled Is the button disabled
   *
   * \param string $name The html name of the input, defaults to modify_state
788
   */
789
  function show_header (string $button_text, string $text, bool $plugin_enabled, bool $button_disabled = FALSE, string $name = 'modify_state'): string
790 791
  {
    if ($button_disabled || ((!$this->acl_is_createable() && !$plugin_enabled) || (!$this->acl_is_removeable() && $plugin_enabled))) {
792
      $state = 'disabled="disabled"';
793
    } else {
794
      $state = '';
795 796
    }
    $display = '<div width="100%"><p><b>'.$text.'</b><br/>'."\n";
797
    $display .= '<input type="submit" formnovalidate="formnovalidate" value="'.$button_text.'" name="'.$name.'" '.$state.'></p></div><hr class="separator"/>';
798 799 800 801

    return $display;
  }

802 803 804 805
  /*! \brief Check if logged in user have enough right to write this attribute value
   *
   * \param mixed $attr Attribute object or name (in this case it will be fetched from attributesAccess)
   */
806
  function attrIsWriteable ($attr): bool
807
  {
808 809 810
    if (!is_object($attr)) {
      $attr = $this->attributesAccess[$attr];
    }
811
    if ($attr->getLdapName() == 'base') {
812 813 814 815
      return (
        !$this->acl_skip_write() &&
        (!$this->initially_was_account || $this->acl_is_moveable() || $this->acl_is_removeable())
      );
816 817 818 819
    }
    return $this->acl_is_writeable($attr->getAcl(), $this->acl_skip_write());
  }

820
  function renderAttributes (bool $readOnly = FALSE)
821
  {
822
    global $ui;
823 824
    $smarty = get_smarty();

825 826 827 828 829 830
    if ($this->is_template) {
      $smarty->assign('template_cnACL', $ui->get_permissions($this->acl_base, $this->acl_category.'template', 'template_cn', $this->acl_skip_write()));
    }

    /* Handle rights to modify the base */
    if (isset($this->attributesAccess['base'])) {
831
      if ($this->attrIsWriteable('base')) {
832 833 834 835 836 837
        $smarty->assign('baseACL', 'rw');
      } else {
        $smarty->assign('baseACL', 'r');
      }
    }

838
    $sections = [];
839 840 841
    foreach ($this->attributesInfo as $section => $sectionInfo) {
      $legend = $sectionInfo['name'];
      if (isset($sectionInfo['icon'])) {
Côme Chilliet's avatar
Côme Chilliet committed
842 843
        $legend = '<img '.
                  'src="'.htmlentities($sectionInfo['icon'], ENT_COMPAT, 'UTF-8').'" '.
844
                  'alt="" '.
Côme Chilliet's avatar
Côme Chilliet committed
845
                  '/>'.$legend;
846 847 848
      }
      $smarty->assign("section", $legend);
      $smarty->assign("sectionId", $section);
849 850 851 852 853
      if (isset($sectionInfo['class'])) {
        $smarty->assign("sectionClasses", ' '.join(' ', $sectionInfo['class']));
      } else {
        $smarty->assign("sectionClasses", '');
      }
854
      $attributes = [];
855 856 857
      foreach ($sectionInfo['attrs'] as $attr) {
        if ($attr->getAclInfo() !== FALSE) {
          // We assign ACLs so that attributes can use them in their template code
858
          $smarty->assign($attr->getAcl()."ACL", $this->aclGetPermissions($attr->getAcl(), NULL, $this->acl_skip_write()));
859
        }
860
        $attr->renderAttribute($attributes, $readOnly);
861 862 863 864 865 866 867 868 869 870 871
      }
      $smarty->assign("attributes", $attributes);
      // We fetch each section with the section template
      if (isset($sectionInfo['template'])) {
        $displaySection = $smarty->fetch($sectionInfo['template']);
      } else {
        $displaySection = $smarty->fetch(get_template_path('simpleplugin_section.tpl'));
      }
      $sections[$section] = $displaySection;
    }
    $smarty->assign("sections", $sections);
872 873
  }

874
  function inheritanceDisplay (): string
875 876 877 878 879 880 881
  {
    if (!$this->member_of_group) {
      return "";
    }
    $class = get_class($this);
    $attrsWrapper = new stdClass();
    $attrsWrapper->attrs = $this->group_attrs;
882
    $group = new $class($this->group_attrs['dn'], $attrsWrapper, $this->parent, $this->mainTab);
883 884
    $smarty = get_smarty();

885
    $group->renderAttributes(TRUE);
886 887
    $smarty->assign("hiddenPostedInput", get_class($this)."_posted");

888
    return "<h1>Inherited information:</h1><div></div>\n".$smarty->fetch($this->templatePath);
889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906
  }

  /*! \brief This function allows you to open a dialog
   *
   *  \param mixed $dialog The dialog object
   */
  function openDialog ($dialog)
  {
    $this->dialog = $dialog;
  }

  /*! \brief This function closes the dialog
   */
  function closeDialog ()
  {
    $this->dialog = NULL;
  }

907
  public function setNeedEditMode (bool $bool)
908 909 910 911
  {
    $this->needEditMode = $bool;
  }

912
  protected function acl_skip_write (): bool
913
  {
914
    return ($this->needEditMode && !session::is_set('edit'));
915 916
  }

917
  /*! \brief Can we write the attribute */
918
  function acl_is_writeable ($attribute, bool $skipWrite = FALSE): bool
919
  {
920
    return preg_match('/w/', $this->aclGetPermissions($attribute, NULL, $skipWrite));
921 922 923 924 925 926 927
  }

  /*!
   * \brief Can we read the acl
   *
   * \param string $attribute
   */
928
  function acl_is_readable ($attribute): bool
929 930 931 932 933 934 935 936 937
  {
    return preg_match('/r/', $this->aclGetPermissions($attribute));
  }

  /*!
   * \brief Can we create the object
   *
   * \param string $base Empty string
   */
938
  function acl_is_createable (string $base = NULL): bool
939 940 941 942 943 944 945 946 947
  {
    return preg_match('/c/', $this->aclGetPermissions('0', $base));
  }

  /*!
   * \brief Can we delete the object
   *
   * \param string $base Empty string
   */
948
  function acl_is_removeable (string $base = NULL): bool
949 950 951 952 953 954 955 956 957
  {
    return preg_match('/d/', $this->aclGetPermissions('0', $base));
  }

  /*!
   * \brief Can we move the object
   *
   * \param string $base Empty string
   */
958
  function acl_is_moveable (string $base = NULL): bool
959 960 961 962 963
  {
    return preg_match('/m/', $this->aclGetPermissions('0', $base));
  }

  /*! \brief Get the acl permissions for an attribute or the plugin itself */
964
  function aclGetPermissions ($attribute = '0', string $base = NULL, bool $skipWrite = FALSE): string
965
  {
966 967 968
    if (isset($this->parent) && isset($this->parent->ignoreAcls) && $this->parent->ignoreAcls) {
      return 'cdmr'.($skipWrite ? '' : 'w');
    }
969 970 971 972 973 974 975 976
    $ui         = get_userinfo();
    $skipWrite  |= $this->readOnly();
    if ($base === NULL) {
      $base = $this->acl_base;
    }
    return $ui->get_permissions($base, $this->acl_category.get_class($this), $attribute, $skipWrite);
  }

977 978
  /*! \brief This function removes the object from LDAP
   */
979
  function remove (bool $fulldelete = FALSE): array
980
  {
981
    if (!$this->initially_was_account) {
982
      return [];
983
    }
984 985 986

    if (!$fulldelete && !$this->acl_is_removeable()) {
      trigger_error('remove was called on a tab without enough ACL rights');
987
      return [];
988 989
    }

990
    $this->prepare_remove();
991 992
    if ($this->is_template && (!defined('_OLD_TEMPLATES_') || !_OLD_TEMPLATES_)) {
      $this->attrs = $this->templateSaveAttrs();
993
      $this->saved_attributes = [];
994
    }
995 996 997 998 999
    /* Pre hooks */
    $errors = $this->pre_remove();
    if (!empty($errors)) {
      return $errors;
    }
1000 1001 1002
    $errors = $this->ldap_remove();
    if (!empty($errors)) {
      return $errors;
1003
    }
1004
    $this->post_remove();
1005
    return [];
1006
  }
1007

1008
  /* Remove FusionDirectory attributes */
1009 1010
  protected function prepare_remove ()
  {
1011
    global $config;
1012
    $this->attrs = [];
1013

1014 1015
    if (!$this->mainTab) {
      /* include global link_info */
1016
      $ldap = $config->get_ldap_link();
1017 1018 1019

      /* Get current objectClasses in order to add the required ones */
      $ldap->cat($this->dn);
1020
      $tmp  = $ldap->fetch();
1021
      $oc   = [];
1022 1023 1024 1025 1026 1027
      if ($this->is_template) {
        if (isset($tmp['fdTemplateField'])) {
          foreach ($tmp['fdTemplateField'] as $tpl_field) {
            if (preg_match('/^objectClass:(.+)$/', $tpl_field, $m)) {
              $oc[] = $m[1];
            }
1028 1029
          }
        }
1030 1031 1032 1033 1034
      } else {
        if (isset($tmp['objectClass'])) {
          $oc = $tmp['objectClass'];
          unset($oc['count']);
        }
1035 1036
      }

1037 1038
      /* Remove objectClasses from entry */
      $this->attrs['objectClass'] = array_remove_entries_ics($this->objectclasses, $oc);
1039

1040 1041
      /* Unset attributes from entry */
      foreach ($this->attributes as $val) {
1042
        $this->attrs["$val"] = [];
1043
      }
1044
    }
1045
  }
1046

1047 1048
  protected function pre_remove ()
  {
1049
    if ($this->initially_was_account) {
1050
      return $this->handle_pre_events('remove', ['modifiedLdapAttrs' => array_keys($this->attrs)]);
1051
    }
1052
  }
1053

1054
  protected function ldap_remove (): array
1055
  {
1056 1057
    global $config;
    $ldap = $config->get_ldap_link();
1058 1059 1060 1061 1062 1063 1064
    if ($this->mainTab) {
      $ldap->rmdir_recursive($this->dn);
    } else {
      $this->cleanup();
      $ldap->cd($this->dn);
      $ldap->modify($this->attrs);
    }
1065
    $this->ldap_error = $ldap->get_error();
1066

1067
    if ($ldap->success()) {
1068
      return [];
1069
    } else {
1070
      return [msgPool::ldaperror($this->ldap_error, $this->dn, LDAP_MOD, get_class())];
1071
    }
1072
  }
1073

1074
  protected function post_remove ()
1075
  {
1076
    logging::log('remove', 'plugin/'.get_class($this), $this->dn, array_keys($this->attrs), $this->ldap_error);
1077 1078

    /* Optionally execute a command after we're done */
1079
    $errors = $this->handle_post_events('remove', ['modifiedLdapAttrs' => array_keys($this->attrs)]);
1080 1081 1082
    if (!empty($errors)) {
      msg_dialog::displayChecks($errors);
    }
1083 1084 1085 1086 1087 1088
  }

  /*! \brief This function handle $_POST informations
   */
  function save_object ()
  {
1089
    @DEBUG(DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, $this->dn, 'save_object');
1090 1091 1092 1093 1094 1095 1096
    if ($this->displayHeader && isset($_POST[get_class($this).'_modify_state'])) {
      if ($this->is_account && $this->acl_is_removeable()) {
        $this->is_account = FALSE;
      } elseif (!$this->is_account && $this->acl_is_createable()) {
        $this->is_account = TRUE;
      }
    }
1097
    if (isset($_POST[get_class($this).'_posted'])) {
1098 1099
      // If our form has been posted
      // A first pass that loads the post values
1100 1101
      foreach ($this->attributesInfo as $sectionInfo) {
        foreach ($sectionInfo['attrs'] as $attr) {
1102
          if ($this->attrIsWriteable($attr)) {
1103 1104 1105 1106 1107 1108
            // Each attribute know how to read its value from POST
            $attr->loadPostValue();
          }
        }
      }
      // A second one that applies them. That allow complex stuff such as attribute disabling
1109 1110
      foreach ($this->attributesInfo as $sectionInfo) {
        foreach ($sectionInfo['attrs'] as $attr) {
1111
          if ($this->attrIsWriteable($attr)) {
1112 1113 1114 1115 1116 1117 1118 1119
            // Each attribute know how to read its value from POST
            $attr->applyPostValue();
          }
        }
      }
    }
  }

1120
  protected function prepareSavedAttributes ()
1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153
  {
    /* Prepare saved attributes */
    $this->saved_attributes = $this->attrs;
    foreach (array_keys($this->saved_attributes) as $index) {
      if (is_numeric($index)) {
        unset($this->saved_attributes[$index]);
        continue;
      }

      if (!in_array_ics($index, $this->attributes) && strcasecmp('objectClass', $index)) {
        unset($this->saved_attributes[$index]);
        continue;
      }

      if (isset($this->saved_attributes[$index][0])) {
        if (!isset($this->saved_attributes[$index]['count'])) {
          $this->saved_attributes[$index]['count'] = count($this->saved_attributes[$index]);
        }
        if ($this->saved_attributes[$index]['count'] == 1) {
          $tmp = $this->saved_attributes[$index][0];
          unset($this->saved_attributes[$index]);
          $this->saved_attributes[$index] = $tmp;
          continue;
        }
      }
      unset($this->saved_attributes[$index]['count']);
    }
  }

  /*!
   * \brief Remove attributes, empty arrays, arrays
   * single attributes that do not differ
   */
1154
  function cleanup ()
1155 1156 1157 1158 1159 1160
  {
    foreach ($this->attrs as $index => $value) {

      /* Convert arrays with one element to non arrays, if the saved
         attributes are no array, too */
      if (is_array($this->attrs[$index]) &&
1161
          (count($this->attrs[$index]) == 1) &&
1162 1163 1164 1165 1166 1167 1168
          isset($this->saved_attributes[$index]) &&
          !is_array($this->saved_attributes[$index])) {
        $this->attrs[$index] = $this->attrs[$index][0];
      }

      /* Remove emtpy arrays if they do not differ */
      if (is_array($this->attrs[$index]) &&
1169
          (count($this->attrs[$index]) == 0) &&
1170
          !isset($this->saved_attributes[$index])) {
Côme Chilliet's avatar
Côme Chilliet committed
1171
        unset($this->attrs[$index]);
1172 1173 1174 1175 1176 1177 1178
        continue;
      }

      /* Remove single attributes that do not differ */
      if (!is_array($this->attrs[$index]) &&
          isset($this->saved_attributes[$index]) &&
          !is_array($this->saved_attributes[$index]) &&
1179
          ($this->attrs[$index] == $this->saved_attributes[$index])) {
Côme Chilliet's avatar
Côme Chilliet committed
1180
        unset($this->attrs[$index]);
1181 1182 1183 1184 1185 1186
        continue;
      }

      /* Remove arrays that do not differ */
      if (is_array($this->attrs[$index]) &&
          isset($this->saved_attributes[$index]) &&
1187 1188
          is_array($this->saved_attributes[$index]) &&
          !array_differs($this->attrs[$index], $this->saved_attributes[$index])) {
Côme Chilliet's avatar
Côme Chilliet committed
1189
        unset($this->attrs[$index]);
1190
        continue;
1191 1192 1193 1194
      }
    }
  }

1195
  function prepareNextCleanup ()
1196 1197 1198 1199 1200 1201 1202
  {
    /* Update saved attributes and ensure that next cleanups will be successful too */
    foreach ($this->attrs as $name => $value) {
      $this->saved_attributes[$name] = $value;
    }
  }

1203 1204
  /*! \brief This function saves the object in the LDAP
   */
1205
  function save (): array
1206
  {
1207
    @DEBUG(DEBUG_TRACE, __LINE__, __FUNCTION__, __FILE__, $this->dn, "save");
1208 1209 1210 1211
    $errors = $this->prepare_save();
    if (!empty($errors)) {
      return $errors;
    }
1212
    if ($this->is_template && (!defined('_OLD_TEMPLATES_') || !_OLD_TEMPLATES_)) {
1213 1214 1215 1216
      $errors = templateHandling::checkFields($this->attrs);
      if (!empty($errors)) {
        return $errors;
      }
1217
      $this->attrs = $this->templateSaveAttrs();
1218
      $this->saved_attributes = [];
1219
    }
1220 1221
    $this->cleanup();
    if (!$this->shouldSave()) {
1222
      return []; /* Nothing to do here */
1223 1224 1225 1226 1227 1228 1229
    }
    /* Pre hooks */
    $errors = $this->pre_save();
    if (!empty($errors)) {
      return $errors;
    }
    /* LDAP save itself */
1230
    $errors = $this->ldap_save();
1231 1232 1233
    if (!empty($errors)) {
      return $errors;
    }
1234
    $this->prepareNextCleanup();
1235
    /* Post hooks and logging */
1236
    $this->post_save();
1237
    return [];
1238 1239
  }

1240
  protected function shouldSave (): bool
1241 1242 1243 1244 1245
  {
    if ($this->mainTab && !$this->initially_was_account) {
      return TRUE;
    }
    return !empty($this->attrs);
1246 1247
  }

1248
  /* Used by prepare_save and template::apply */
1249
  public function mergeObjectClasses (array $oc): array
1250 1251 1252 1253
  {
    return array_merge_unique($oc, $this->objectclasses);
  }

1254
  protected function prepare_save (): array
1255
  {
1256
    global $config;