Bitrix Auditoria do tipo faça você mesmo

Olá a todos.

Quando eu estava procurando informações sobre registro (auditoria de eventos) no Bitrix, não havia nada no Habr, mas havia algo mais no restante, mas quem o encontrará lá?

Para reabastecer a base de conhecimento, decidi escrever este artigo: compartilhar minha experiência e avisar sobre um possível rake.

Formulação do problema


Minha tarefa era desenvolver o sistema contábil mais simples para estruturas de publicidade, nos termos do contrato estadual, o sistema deveria funcionar com base no Bitrix (versão 15).

Era possível pedalar tudo do lado do Bitrix, mas eu decidi que seria muito desonesto em relação ao cliente, a funcionalidade do Bitrix era usada ao máximo:

  • autenticação de usuário
  • sistema de armazenamento de dados (EAV)
  • editor de dados
  • manipuladores de eventos de auditoria
  • modelo para autorizar ações do usuário
  • gerenciamento de usuários
  • trabalhar com diretórios

Este artigo é principalmente sobre eventos de auditoria, também falarei um pouco sobre autorização e adição de marcadores personalizados ao cartão de registro do bloco de informações.

Eventos de auditoria


No sistema desenvolvido, o trabalho com dados é realizado através da API (adicionando um novo objeto ao mapa e alterando suas coordenadas), outros parâmetros do objeto são editados através do editor Bitrix, uma vez que a API não processa todas as solicitações de alteração de dados, a auditoria apenas na API não seria concluída, o que significa precisamos de uma auditoria ao lado do Bitrix.

Não é que eu não sabia o que é auditoria, mas não preciso escrevê-la com frequência. Geralmente, isso é resolvido gravando a versão antiga da linha (dados antigos) em uma tabela separada e a implementação é realizada inteiramente no lado do DBMS. Além disso, temos simultaneamente um estado antes das mudanças e um estado depois das mudanças.

Para escrever uma auditoria nos gatilhos do DBMS ou para escrever uma auditoria nos eventos do Bitrix, a diferença não é grande.
Eventos para processamento são registrados no script "bitrix / php_interface / init.php ", se não houver arquivo, você precisará criá-lo:

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'));
//     ,  -    ,              ,   

Este é um exemplo de um manipulador, na minha implementação existem vários deles, o código pode ser visualizado no repositório, o link estará no final do artigo.

A assinatura do método para processar o evento para cada evento possui uma característica própria, que analisamos especificamente na documentação do Bitrix ou lançamos e analisamos as fontes (mais claramente, todos os eventos e momentos dos chamadores de tratamento também podem ser vistos nas fontes durante a depuração).

E aqui nos deparamos com o fato de que, ao trabalhar com um DBMS, temos dados atuais (antigos) e os dados que serão registrados (novos), e ao trabalhar com o Bitrix, temos dados antigos ou novos, mas não quando não haverá velhos e novos ao mesmo tempo.

Outro problema é que existem vários eventos antes de alterar os dados, e eu não conseguia entender a diferença entre eles, a mesma história é quando vários eventos foram acionados depois que os dados foram alterados, os olhos estão olhando para a riqueza de opções, na verdade, é apenas uma ilusão de escolha.

minha humilde opinião sobre o Bitrix
, , _ , ( Delphi PHP), .

depricated «», .

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

, :)

, , , , , , , ? .


Usando estado global


Para salvar e passar o estado entre manipuladores de eventos, é necessário ter variáveis ​​globais. Variáveis ​​globais são algo que não pode ser refatorado, então eu uso as propriedades estáticas da classe:

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;
}

Aqui você precisa fazer uma explicação sobre o uso de "campos" e "propriedades". Campos são os parâmetros que cada registro de infobloco possui (identificador, nome, infobloco "pai"), propriedades são parâmetros específicos para registros de um tipo específico de infobloco, no modelo EAV, esses são atributos e seus valores.

"Nomes" são nomes de atributos, em um evento temos identificadores de atributos e nomes, em outro evento temos apenas identificadores e, para um registro bonito no diário, precisamos salvar os nomes (é claro que podemos subtrair o identificador, mas por algum motivo você não deseja).

"Operação" é a operação atual, para trabalhar com "campos", o Bitrix fornece eventos específicos, ao trabalhar com "propriedades" (funções "SetPropertyValues" e "SetPropertyValuesEx"), não há evento com o tipo de operação, há um evento antes e depois da chamada, mas não ocorre Sabe-se qual operação é executada (adicionar / atualizar / excluir), portanto, a operação também precisa ser transferida do manipulador para o manipulador.

Talvez eu não tenha descoberto, e no Bitrix você pode ver simultaneamente os valores como eram e como serão, ou talvez o estado antes e o estado depois devam ser registrados separadamente, mas por algum motivo eu decidi que a entrada do log deveria estar no formato "propriedade % name% era% value antes da alteração% => tornou-se% value após% ”e, portanto, há muitos problemas com a manutenção do estado global.

Determinando se são necessárias alterações


Curiosamente, mas nosso manipulador será chamado para alterar qualquer registro de qualquer bloco de informações.
Portanto, no manipulador, precisamos entender o registro de qual bloco de informações recebemos nos parâmetros.
O infobloco de um registro é caracterizado por dois valores: o próprio infobloco ('IBLOCK_ID') e sua seção ('IBLOCK_SECTION_ID'), e o identificador da seção às vezes fica na matriz de valores com o índice 'IBLOCK_SECTION_ID' e, às vezes, 'IBLOCK_SECTION', então eu li o identificador da seção por ambas as chaves com prioridade para 'IBLOCK_SECTION'.

Além disso, em algumas situações, o identificador da seção não pode ser determinado em princípio; portanto, a necessidade de adicionar uma entrada ao log de auditoria deve ser verificada apenas com base no identificador do bloco de informações.

Após determinar o bloco e a seção de informações, decidimos sobre a necessidade de registrar o evento no diário.

Salvando o estado antes das alterações


Cada entrada de infobloco consiste em duas partes, uma parte é "campos" e esses parâmetros são alterados pelos métodos:

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

A outra parte são os métodos de "propriedades":
  • CIBlockElement :: SetPropertyValues ​​()
  • CIBlockElement :: SetPropertyValuesEx ()

Não me lembro por que, mas é melhor não usar CIBlockElement :: SetPropertyValuesEx () no meu código.

Cada parte dos dados em seu manipulador de eventos é separada da outra, portanto, clicando no botão "Salvar", duas entradas no log de auditoria podem ser criadas.

As assinaturas de eventos para "campos" e "propriedades" são diferentes; na parte com o processamento de "campos", salvamos o estado atual dos campos e definimos o valor para operação; na parte com o processamento de "propriedades", salvamos as propriedades e seus nomes.

É claro que fazemos isso no manipulador "antes".

Alterar registro


No manipulador "after", escrevemos no log.

Ao compilar uma lista de diferenças nos registros, há um problema com os atributos que podem assumir vários valores. O formato para registrar seu estado “antes” e “depois” difere tanto que um não pode deduzir o outro; portanto, ao determinar se deseja registrar alterações, você sempre concluirá que o registro é necessário.
O código-fonte da auditoria no repositório no arquivo src / Topliner / Scheme / Logger.php .

Adicionando uma guia personalizada ao cartão de infobloco (no classificador Bitrix)


Para adicionar um cartão, é utilizada uma abordagem semelhante à auditoria - adicionamos um manipulador:

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

No método 'OnInit', definimos os outros métodos para trabalhar com a guia no editor Bitrix:

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;
    }
}

Primeiro, é claro, verificamos que, para esse bloco de informações, precisamos mostrar nossa guia do usuário, se necessário, definimos métodos.

Não há nada especial para contar aqui, consulte as fontes no repositório src / Topliner / Scheme / PermitTab.php , leia a documentação.

Modelo de função para autorizar ações do usuário


Os métodos de autorização da Bitrix funcionam com uma escala linear de direitos, ou seja, um cenário em que um pode ser o primeiro e o segundo, mas não o terceiro, e o outro pode ser o segundo e o terceiro, mas não o primeiro, não é possível implementar o Bitrix diretamente.

Para cenários tão complicados no Bitrix, há simplesmente uma verificação de alguma permissão e, no código ao executar a operação, você vê se o usuário tem permissão ou não, e permite a primeira e a segunda funções, e a segunda e a terceira permitem a outra.

        $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 () - identificador do bloco de informações
  2. $ constSec-> getSection () - identificador da seção
  3. BitrixPermission :: ELEMENT_ADD - código de permissão

No classificador (diretório do bloco de informações) nos blocos e seções de informações necessárias, você atribui os direitos apropriados às funções. Distribua funções para os usuários e tudo estará sob seu controle.

Autenticação


Se você precisar de autenticação de usuário para algumas páginas, basta adicionar:

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

//      

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

Ao carregar essa página, o Bitrix fornecerá um formulário de login se o usuário ainda não estiver conectado.

Se você só precisa conectar o Bitrix em algum tipo de script, então:

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

Trabalhar com diretórios


Os diretórios “novos” (a partir de novembro de 2019) no Bitrix são chamados de “HighloadBlock”, trabalhar com eles é um pouco fora do padrão.

Cada bloco Hyload pode ser armazenado em uma tabela separada, portanto, para acessar o diretório, sempre que você precisar determinar o nome da tabela a ser lida. Para não fazer isso constantemente, no Bitrix você precisa criar uma instância de uma classe para acessar dados. Uma instância é criada em duas linhas:

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

Onde 'ConstructionTypes' é o código de referência. Para

não escrever constantemente essas duas linhas, você pode escrever uma classe que criará instâncias de referência para nós ( src / Topliner / Bitrix / BitrixReference.php ).

Em seguida, trabalhamos com a instância como com a classe Bitrix ORM usual (D7):

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

Repositório (com cuidado, muita copiar e colar)

Obrigado por sua atenção.

All Articles