Bitrix Do-it-yourself audit

Hello everyone.

When I was looking for information about logging (audit of events) in Bitrix, there wasn’t anything on Habr, but there were something else in the rest, but who will find it there?

To replenish the knowledge base, I decided to write this article: share my experience and warn about a possible rake.

Formulation of the problem


My task was to develop the simplest accounting system for advertising structures, under the terms of the state contract, the system should work on the basis of Bitrix (version 15).

It was possible to bike everything on the side of Bitrix, but I decided that it would be too dishonest in relation to the customer, the functionality of Bitrix was used to the maximum:

  • user authentication
  • data storage system (EAV)
  • data editor
  • audit event handlers
  • role model for authorizing user actions
  • user management
  • work with directories

This article is mainly about audit events, I will also talk a bit about authorization and adding custom bookmarks to the info block record card.

Audit Events


In the developed system, work with data is carried out through the API (adding a new object to the map and changing its coordinates), other parameters of the object are edited through the Bitrix editor, since the API does not process all data change requests, so the audit only in the API would not be complete, which means we need an audit on the side of Bitrix.

It’s not that I didn’t know what audit is, but I don’t have to write it often. Usually this is solved by writing the old version of the row (old data) into a separate table, and the implementation is performed entirely on the DBMS side. Moreover, we simultaneously have a state before the changes and a state after the changes.

To write an audit on DBMS triggers, or to write an audit on Bitrix events, the difference is not big.
Events for processing are registered in the script "bitrix / php_interface / init.php ", if there is no file, then you need to create it:

use Topliner\Scheme\Logger;
//  
require_once($_SERVER['DOCUMENT_ROOT'] . '/vendor/autoload.php');
//        
if (!defined('IBLOCK_MODULE')) {
    define('IBLOCK_MODULE', 'iblock');
}
//    
AddEventHandler(IBLOCK_MODULE, 'OnIBlockElementAdd',
    Array(Logger::class, 'OnAdd'));
//     ,  -    ,              ,   

This is an example of one handler, in my implementation there are several of them, the code can be viewed in the repository, the link will be at the end of the article.

The signature of the method for processing the event for each event has its own, which we specifically look at in the Bitrix documentation or debut and look at the sources (more clearly, all events and moments of calling handlers can also be seen in the sources during debugging).

And here we are faced with the fact that when working with a DBMS, we have both current (old) data and the data that will be recorded (new), and when working with Bitrix, we have either old data or new data, but not when there will be no old and new at the same time.

Another problem is that there are several events before changing the data, and I could not understand the difference between them, the same story is about the triggering of several events after changing the data, eyes are staring from the wealth of choice, in fact it is just an illusion of choice.

my humble opinion about Bitrix
, , _ , ( Delphi PHP), .

depricated «», .

« » ( ) «» (, , ) , . , «» .

, :)

, , , , , , , ? .


Using global state


To save and pass state between event handlers, you have to have global variables. Global variables are something that cannot be refactored, so I use the static properties of the class:

class Logger
{
    const CHANGE = 'change';
    const REMOVE = 'remove';
    const CREATE = 'create';
    const UNDEFINED = 'undefined';

    public static $operation = self::UNDEFINED;

    const NO_VALUE = [];

    private static $fields = self::NO_VALUE;
    private static $properties = self::NO_VALUE;
    private static $names = self::NO_VALUE;
}

Here you need to make an explanation about the use of "fields" and "properties". Fields are the parameters that each infoblock record has (identifier, name, infoblock “parent”), properties are parameters specific to records of a particular type of infoblock, in the EAV model, these are attributes and their values.

“Names” are the names of attributes, in one event we have identifiers of attributes and names, in another event we have only identifiers, and for a beautiful record in the journal we need to save the names (we can of course subtract the identifier, but for some reason you don’t want to).

"Operation" is the current operation, for working with "fields" Bitrix gives specific events, when working with "properties" (functions "SetPropertyValues" and "SetPropertyValuesEx"), there is no event with the type of operation, there is an event before and after the call, but it does not It is known what operation is performed (add / update / delete), therefore the operation also needs to be transferred from the handler to the handler.

Maybe I didn’t figure it out, and in Bitrix you can simultaneously see the values ​​as it was and how it will be, or maybe it was necessary to separately record the state before and the state after, but for some reason I decided that the log entry should be in the format “property % name% was% value before the change% => it became% value after% ”and therefore there are a lot of problems with maintaining the global state.

Determining if changes are required


Oddly enough, but our handler will be called to change any record of any information block.
Therefore, in the handler we need to understand the record of which information block we received in the parameters.
The infoblock of a record is characterized by two values: the infoblock itself ('IBLOCK_ID') and its section ('IBLOCK_SECTION_ID'), and the section identifier sometimes lies in the array of values ​​with the index 'IBLOCK_SECTION_ID', and sometimes 'IBLOCK_SECTION', so I read the section identifier by both keys with priority for 'IBLOCK_SECTION'.

Moreover, in some situations, the section identifier cannot be determined in principle, therefore, the check for the need to add an entry to the audit log has to be done only on the basis of the information block identifier.

After determining the information block and section, we decide on the need to register the event in the journal.

Saving state before changes


Each infoblock entry consists of two parts, one part is “fields”, and these parameters are changed by methods:

  • CIBlockElement :: Add ()
  • CIBlockElement :: Update ()
  • CIBlockElement :: Delete ()

The other part is the “properties” methods:
  • CIBlockElement :: SetPropertyValues ​​()
  • CIBlockElement :: SetPropertyValuesEx ()

I don’t remember why, but it's better to refrain from using CIBlockElement :: SetPropertyValuesEx () in my code.

Each part of the data in its event handler is separate from the other, therefore, by clicking the "Save" button, two entries in the audit log can be created.

The signatures of the events for “fields” and “properties” are different, in the part with processing “fields” we save the current state for fields and set the value to operation, in the part with processing “properties” we save properties and their names.

We do this of course in the "before" handler.

Change Registration


In the "after" handler, we write to the log.

When compiling a list of differences in records, there is a problem with attributes that can take multiple values. The format of recording their state “before” and state “after” differs so much that one cannot deduce the other, so when determining whether to register changes, you will always conclude that registration is necessary.
The source code of the audit in the repository in the src / Topliner / Scheme / Logger.php file .

Adding a custom tab to the infoblock card (in the Bitrix classifier)


To add a card, an approach similar to auditing is used - we add a handler:

AddEventHandler('main', 'OnAdminIBlockElementEdit',
    Array(PermitTab::class, 'OnInit'));

In the 'OnInit' method, we define the other methods for working with the tab in the Bitrix editor:

class PermitTab
{
    const LINKS = 'links';

    function OnInit($arArgs)
    {
        $permits = BitrixScheme::getPermits();
        $pubPermits = BitrixScheme::getPublishedPermits();
        $blockId = (int)$arArgs['IBLOCK']['ID'];
        $letShow = $blockId === $permits->getBlock()
            || $blockId === $pubPermits->getBlock();
        $result = false;
        if ($letShow) {
            $result = [
                'TABSET' => 'LinksToConstructs',
                'GetTabs' => [static::class, 'GetTabs'],
                'ShowTab' => [static::class, 'ShowTab'],
                'Action' => [static::class, 'Action'],
                'Check' => [static::class, 'Check'],
            ];
        }

        return $result;
    }
}

First, of course, we check that for this information block we need to show our user tab, if necessary, we set methods.

There is nothing special to tell here, see the sources in the src / Topliner / Scheme / PermitTab.php repository , read the documentation.

Role model for authorizing user actions


Bitrix’s methods for authorization work with a linear rights scale, that is, a scenario where one can have the first and second, but not the third, and the other can be the second and third, but not the first, you can’t implement Bitrix directly.

For such tricky scenarios in Bitrix, there is simply a check of some permission, and in the code when performing the operation, you see if the user has permission or not, and you allow the first and second roles of one role and the second and third ones of the role.

        $constSec = BitrixScheme::getConstructs();
        $isAllow = CIBlockSectionRights::UserHasRightTo(
            $constSec->getBlock(), $constSec->getSection(),
            BitrixPermission::ELEMENT_ADD, false);
        if (!$isAllow) {
            $output['message'] = 'Forbidden, not enough permission;';
        }

  1. $ constSec-> getBlock () - information block identifier
  2. $ constSec-> getSection () - section identifier
  3. BitrixPermission :: ELEMENT_ADD - permission code

In the classifier (information block directory) in the necessary information blocks and sections, you assign the appropriate rights to the roles. Distribute roles to users and everything will be under your control.

Authentication


If you need user authentication for some pages, just add:

<?php
define("NEED_AUTH", true);
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/header.php");
?>

//      

<?php
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/footer.php");
?>

When loading such a page, Bitrix will give out a login form if the user has not logged in yet.

If you just need to connect Bitrix in some kind of script, then:

require_once($_SERVER['DOCUMENT_ROOT']
    . '/bitrix/modules/main/include/prolog_before.php');

Work with directories


The “new” (as of November 2019) directories in Bitrix are called “HighloadBlock”, working with them is a little bit non-standard.

Each Hyload block can be stored in a separate table, therefore, to access the directory, each time you need to determine the name of the table to read. In order not to constantly do this, in Bitrix you need to create an instance of a class for accessing data. An instance is created in two lines:

CModule::IncludeModule('highloadblock');
$entity = HighloadBlockTable::compileEntity('ConstructionTypes');
$reference = $entity->getDataClass();

Where 'ConstructionTypes' is the reference code.

In order not to constantly write these two lines, you can write a class that will create reference instances for us ( src / Topliner / Bitrix / BitrixReference.php ).

Next, we work with the instance as with the usual Bitrix ORM class (D7):

/* @var $reference DataManager */
$data = $reference::getList(array(
    'select' => array('UF_NAME', 'UF_XML_ID'),
    'filter' => array('UF_TYPE_ID' => 0)
    ));

Repository (carefully, a lot of copy-paste)

Thank you for your attention.

All Articles