This commit is contained in:
root
2025-11-13 19:52:28 +03:00
parent 8aeeb05b7d
commit 807dec3b6c
4646 changed files with 163445 additions and 626017 deletions

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Bitrix\Landing\Copilot\Connector\AI;
use Bitrix\Landing\Copilot\Connector\AI\Type\LimitType;
use Bitrix\Landing\Metrika\Statuses;
/**
* Data transfer object representing the result of a limit check for AI requests.
*
* Contains information about the type of limit, a localized message for the user,
* and a flag indicating whether the limit has been exceeded.
*
* @property LimitType $type The type of the limit that was checked.
* @property string|null $message Localized message describing the limit status.
* @property bool $isExceeded Whether the limit has been exceeded.
*/
class LimitCheckResult
{
public LimitType $type;
public ?string $message;
public bool $isExceeded;
/**
* LimitCheckResult constructor.
*
* @param LimitType $type The type of the limit that was checked.
* @param bool $isExceeded Whether the limit has been exceeded.
* @param string|null $message Localized message describing the limit status.
*/
public function __construct(LimitType $type, bool $isExceeded, ?string $message = null)
{
$this->type = $type;
$this->isExceeded = $isExceeded;
$this->message = $message;
}
/**
* Returns the type of the limit that was checked.
*
* @return LimitType|null The type of the limit.
*/
public function getType(): ?LimitType
{
return $this->type;
}
/**
* Returns whether the limit has been exceeded.
*
* @return bool True if the limit is exceeded, false otherwise.
*/
public function isExceeded(): bool
{
return $this->isExceeded;
}
/**
* Returns the localized message describing the limit status.
*
* @return string|null Localized message.
*/
public function getMessage(): ?string
{
return $this->message;
}
/**
* Returns the metrika status corresponding to the limit type.
*
* @return Statuses Metrika status for analytics.
*/
public function getMetrikaStatus(): Statuses
{
return match ($this->getType())
{
LimitType::Baas => Statuses::ErrorLimitBaas,
LimitType::Daily => Statuses::ErrorLimitDaily,
LimitType::Monthly => Statuses::ErrorLimitMonthly,
LimitType::Market => Statuses::ErrorMarket,
default => Statuses::ErrorB24,
};
}
}

View File

@@ -6,6 +6,14 @@ namespace Bitrix\Landing\Copilot\Connector\AI;
use Bitrix\AI\Cloud;
use Bitrix\AI\Context;
use Bitrix\AI\Facade\Bitrix24;
use Bitrix\Landing\Copilot\Connector\AI\Type\ErrorCode;
use Bitrix\Landing\Copilot\Connector\AI\Type\HelpdeskCode;
use Bitrix\Landing\Copilot\Connector\AI\Type\LimitType;
use Bitrix\Landing\Copilot\Connector\AI\Type\MessageCode;
use Bitrix\Landing\Copilot\Connector\AI\Type\PromoLimitCode;
use Bitrix\Landing\Copilot\Connector\AI\Type\SliderCode;
use Bitrix\AI\Limiter\Enums\TypeLimit;
use Bitrix\AI\Limiter\LimitControlService;
use Bitrix\AI\Limiter\LimitControlBoxService;
@@ -26,196 +34,234 @@ use Psr\Container\NotFoundExceptionInterface;
* Class RequestLimiter
*
* Handles checking and messaging for request quotas to AI services (CoPilot) within the Landing module.
* Supports both cloud (Bitrix24 module present) and box environments.
* Supports both cloud (Bitrix24 module present) and box (on-premise) environments.
* Determines if request limits are exceeded and returns localized error messages or null if within limits.
*/
class RequestLimiter
{
/** @see \Bitrix\AI\Engine::ERRORS (key 'LIMIT_IS_EXCEEDED') */
protected const ERROR_CODE_LIMIT_CLOUD = 'LIMIT_IS_EXCEEDED';
/** @see \Bitrix\AI\Engine\Cloud\CloudEngine::ERROR_CODE_LIMIT_BAAS */
protected const ERROR_CODE_LIMIT_BAAS_CLOUD = 'LIMIT_IS_EXCEEDED_BAAS';
protected const ERROR_CODE_RATE_LIMIT = 'RATE_LIMIT';
protected const ERROR_CODE_DAILY = 'LIMIT_IS_EXCEEDED_DAILY';
protected const ERROR_CODE_MONTHLY = 'LIMIT_IS_EXCEEDED_MONTHLY';
/**
* Slider feature promoter codes for various limit messages
* @var string[]
* Stores the result of the most recent limit check, including type, message, and exceeded flag.
*/
protected const SLIDER_CODES = [
'BOOST_COPILOT' => 'limit_boost_copilot',
'DAILY' => 'limit_copilot_max_number_daily_requests',
'MONTHLY' => 'limit_copilot_requests',
'BOOST_COPILOT_BOX' => 'limit_boost_copilot_box',
'REQUEST_BOX' => 'limit_copilot_requests_box',
'BOX' => 'limit_copilot_box',
];
private LimitCheckResult $checkResult;
/**
* Helpdesk article codes for support links
* @var string[]
* Constructor initializes checkResult with a default value (no limit exceeded).
*/
protected const HELPDESK_CODES = [
'RATE' => '24736310',
];
public function __construct()
{
$this->checkResult = $this->createLimitResult(LimitType::None, false);
}
/**
* Promo limit codes matching Usage::PERIODS values
* @var string[]
* Returns the result object containing information about the current limit check.
*
* @see Usage::PERIODS
* @return LimitCheckResult Limit result data transfer object with type, message, and exceeded flag.
*/
protected const PROMO_LIMIT_CODES = [
'DAILY' => 'Daily',
'MONTHLY' => 'Monthly',
];
public function getCheckResult(): LimitCheckResult
{
return $this->checkResult;
}
/**
* Checks whether an AI service error represents a quota exceed.
* Returns the localized message describing the current limit status.
*
* @param Error $error Error instance returned from the AI service.
*
* @return string|null Localized error message if limit exceeded, or null otherwise.
* @return string|null Localized message for the user about the limit status.
*/
public function getTextFromError(Error $error): ?string
public function getCheckResultMessage(): ?string
{
return $this->checkResult->getMessage();
}
/**
* Checks if the given error corresponds to a request limit exceeded situation.
*
* @param Error $error Error object to check.
*
* @return bool True if the limit is exceeded, false otherwise.
*/
public function checkError(Error $error): bool
{
if (Loader::includeModule('bitrix24'))
{
return $this->getTextFromLimitCloudError($error);
$this->checkResult = $this->checkCloudErrorLimit($error);
}
else
{
$this->checkResult = $this->checkBoxErrorLimit($error);
}
return $this->getTextFromLimitBoxError($error);
return $this->checkResult->isExceeded();
}
/**
* Checks if the request count exceeds quota limits.
*
* @param int $requestCount Number of requests to check.
*
* @return bool True if the limit is exceeded, false otherwise.
*/
public function checkQuota(int $requestCount): bool
{
if ($requestCount <= 0)
{
$this->checkResult = $this->createLimitResult(LimitType::None, false);
return $this->checkResult->isExceeded();
}
if (Loader::includeModule('bitrix24'))
{
$this->checkResult = $this->checkCloudQuotaLimit($requestCount);
}
else
{
$this->checkResult = $this->checkBoxQuotaLimit($requestCount);
}
return $this->checkResult->isExceeded();
}
/**
* Handles cloud-specific AI error codes for quota limits.
*
* @param Error $error Error object from the cloud AI engine.
* @param Error $error Error object to check.
*
* @return string|null Localized message for BAAS, daily, monthly or promo limits, or null if not a quota error.
* @return LimitCheckResult Result object containing a localized message, limit type, and a flag indicating if the limit is exceeded.
*/
protected function getTextFromLimitCloudError(Error $error): ?string
private function checkCloudErrorLimit(Error $error): LimitCheckResult
{
$errorCode = $error->getCode();
$code = $error->getCode();
//right 2 in board
if ($errorCode === self::ERROR_CODE_LIMIT_BAAS_CLOUD)
//right 4 in board
if ($code === ErrorCode::BaasRateLimit->value)
{
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_BAAS',
self::SLIDER_CODES['BOOST_COPILOT'],
return $this->createLimitResult(
LimitType::Rate,
true,
self::buildLimitMessage(MessageCode::BaasRateLimit)
);
}
if (str_starts_with($errorCode, self::ERROR_CODE_LIMIT_CLOUD))
if ($code === ErrorCode::LimitBaasCloud->value)
{
//right 3 in board
if (Bitrix24::isMarketAvailable())
{
return $this->createLimitResult(
LimitType::Market,
true,
self::buildLimitMessage(MessageCode::Market, SliderCode::Market)
);
}
//right 2 in board
return $this->createLimitResult(
LimitType::Baas,
true,
self::buildLimitMessage(MessageCode::Baas, SliderCode::BoostCopilot)
);
}
if (str_starts_with($code, ErrorCode::LimitCloud->value))
{
//right 1 in board
if (Loader::includeModule('baas') && Baas::getInstance()->isAvailable())
{
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_PROMO',
self::SLIDER_CODES['BOOST_COPILOT'],
return $this->createLimitResult(
LimitType::Promo,
true,
self::buildLimitMessage(MessageCode::Promo, SliderCode::BoostCopilot)
);
}
//left 1 in board
if ($errorCode === self::ERROR_CODE_DAILY)
if ($code === ErrorCode::Daily->value)
{
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_DAILY',
self::SLIDER_CODES['DAILY'],
return $this->createLimitResult(
LimitType::Daily,
true,
self::buildLimitMessage(MessageCode::Daily, SliderCode::Daily)
);
}
//left 2 in board
if ($errorCode === self::ERROR_CODE_MONTHLY)
if ($code === ErrorCode::Monthly->value)
{
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_MONTHLY',
self::SLIDER_CODES['MONTHLY'],
return $this->createLimitResult(
LimitType::Monthly,
true,
self::buildLimitMessage(MessageCode::Monthly, SliderCode::Monthly)
);
}
}
return null;
return $this->createLimitResult(LimitType::None, false);
}
/**
* Handles box AI error codes for quota limits.
*
* @param Error $error Error object containing code and optional custom data.
* @param Error $error Error object to check.
*
* @return string Localized message for rate, BAAS, monthly or promo limits.
* @return LimitCheckResult Result object containing a localized message, limit type, and a flag indicating if the limit is exceeded.
*/
protected function getTextFromLimitBoxError(Error $error): string
private function checkBoxErrorLimit(Error $error): LimitCheckResult
{
$customData = $error->getCustomData();
$errorCode = $error->getCode();
if ($errorCode === self::ERROR_CODE_RATE_LIMIT)
if ($errorCode === ErrorCode::RateLimit->value)
{
//top in board
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_RATE',
null,
self::HELPDESK_CODES['RATE'],
return $this->createLimitResult(
LimitType::Rate,
true,
self::buildLimitMessage(MessageCode::Rate, null, HelpdeskCode::Rate)
);
}
$customData = $error->getCustomData();
if (
isset($customData['showSliderWithMsg'])
&& $errorCode === self::ERROR_CODE_LIMIT_BAAS_CLOUD
isset($customData['showSliderWithMsg'])
&& $errorCode === ErrorCode::LimitBaasCloud->value
)
{
//right 2 in board
if ($customData['showSliderWithMsg'] === true)
{
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_BAAS',
self::SLIDER_CODES['BOOST_COPILOT_BOX'],
return $this->createLimitResult(
LimitType::Baas,
true,
self::buildLimitMessage(MessageCode::Baas, SliderCode::BoostCopilotBox)
);
}
//left 1 in board
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_MONTHLY',
self::SLIDER_CODES['REQUEST_BOX'],
return $this->createLimitResult(
LimitType::Monthly,
true,
self::buildLimitMessage(MessageCode::Monthly, SliderCode::RequestBox)
);
}
//right 1 in board
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_PROMO',
self::SLIDER_CODES['BOOST_COPILOT_BOX'],
return $this->createLimitResult(
LimitType::Promo,
true,
self::buildLimitMessage(MessageCode::Promo, SliderCode::BoostCopilotBox)
);
}
/**
* Checks if a batch of AI requests can be sent without exceeding quotas.
*
* @param int $requestCount Number of AI requests planned.
*
* @return string|null Localized error message if quota would be exceeded, or null if allowed.
*/
public function getTextFromCheckLimit(int $requestCount): ?string
{
if (Loader::includeModule('bitrix24'))
{
return $this->getTextFromCheckCloudLimit($requestCount);
}
return $this->getTextFromCheckBoxLimit($requestCount);
}
/**
* Cloud-side reservation of request quota.
*
* @param int $requestCount Number of requests to reserve.
*
* @return string|null Localized message for BAAS, promo, daily or monthly limits, or null if reserved.
* @return LimitCheckResult Result object containing a localized message, limit type, and a flag indicating if the limit is exceeded.
*/
protected function getTextFromCheckCloudLimit(int $requestCount): ?string
private function checkCloudQuotaLimit(int $requestCount): LimitCheckResult
{
$reservedRequest = (new LimitControlService())->reserveRequest(
new Usage(Context::getFake()),
@@ -224,48 +270,73 @@ class RequestLimiter
if ($reservedRequest->isSuccess())
{
return null;
return $this->createLimitResult(LimitType::None, false);
}
$errorLimit = $reservedRequest->getErrorLimit();
//right 4 in board
if ($errorLimit === ErrorLimit::BAAS_RATE_LIMIT)
{
return $this->createLimitResult(
LimitType::Rate,
true,
self::buildLimitMessage(MessageCode::BaasRateLimit)
);
}
$typeLimit = $reservedRequest->getTypeLimit();
//right 2 in board
if ($errorLimit === ErrorLimit::BAAS_LIMIT && Bitrix24::isMarketAvailable())
{
//right 3 in board
return $this->createLimitResult(
LimitType::Market,
true,
self::buildLimitMessage(MessageCode::Market, SliderCode::Market)
);
}
if ($typeLimit === TypeLimit::BAAS)
{
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_BAAS',
self::SLIDER_CODES['BOOST_COPILOT'],
//right 2 in board
return $this->createLimitResult(
LimitType::Baas,
true,
self::buildLimitMessage(MessageCode::Baas, SliderCode::BoostCopilot)
);
}
//right 1 in board
if (Loader::includeModule('baas') && Baas::getInstance()->isAvailable())
{
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_PROMO',
self::SLIDER_CODES['BOOST_COPILOT'],
return $this->createLimitResult(
LimitType::Promo,
true,
self::buildLimitMessage(MessageCode::Promo, SliderCode::BoostCopilot)
);
}
$promoLimitCode = $reservedRequest->getPromoLimitCode();
//left 1 in board
if ($promoLimitCode === self::PROMO_LIMIT_CODES['DAILY'])
if ($promoLimitCode === PromoLimitCode::Daily->value)
{
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_DAILY',
self::SLIDER_CODES['DAILY'],
return $this->createLimitResult(
LimitType::Daily,
true,
self::buildLimitMessage(MessageCode::Daily, SliderCode::Daily)
);
}
//left 2 in board
if ($promoLimitCode === self::PROMO_LIMIT_CODES['MONTHLY'])
if ($promoLimitCode === PromoLimitCode::Monthly->value)
{
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_MONTHLY',
self::SLIDER_CODES['MONTHLY'],
return $this->createLimitResult(
LimitType::Monthly,
true,
self::buildLimitMessage(MessageCode::Monthly, SliderCode::Monthly)
);
}
return null;
return $this->createLimitResult(LimitType::None, false);
}
/**
@@ -273,23 +344,29 @@ class RequestLimiter
*
* @param int $requestCount Number of requests to reserve.
*
* @return string|null Localized message for cloud registration, rate, BAAS, monthly or promo limits, or null if reserved.
* @return LimitCheckResult Result object containing a localized message, limit type, and a flag indicating if the limit is exceeded.
*
* @throws GenerationException When request reservation fails due to argument or object not found exceptions.
*/
protected function getTextFromCheckBoxLimit(int $requestCount): ?string
private function checkBoxQuotaLimit(int $requestCount): LimitCheckResult
{
$cloudConfiguration = new Cloud\Configuration();
$registrationDto = $cloudConfiguration->getCloudRegistrationData();
$cloudConfig = new Cloud\Configuration();
$registrationDto = $cloudConfig->getCloudRegistrationData();
if (!$registrationDto)
{
//top in board
return self::getLimitMessage('LANDING_REQUEST_LIMITER_ERROR_CLOUD_REGISTRATION',);
return $this->createLimitResult(
LimitType::Unregistered,
true,
self::buildLimitMessage(MessageCode::CloudRegistration)
);
}
try
{
$reservedBoxRequest = (new LimitControlBoxService())->isAllowedQuery($requestCount);
}
catch (ArgumentException | ObjectNotFoundException | NotFoundExceptionInterface $e)
catch (ArgumentException|ObjectNotFoundException|NotFoundExceptionInterface)
{
throw new GenerationException(GenerationErrors::notSendRequest);
}
@@ -301,7 +378,7 @@ class RequestLimiter
if ($reservedBoxRequest->isSuccess())
{
return null;
return $this->createLimitResult(LimitType::None, false);
}
$limitError = $reservedBoxRequest->getErrorByLimit();
@@ -309,10 +386,10 @@ class RequestLimiter
if ($limitError === ErrorLimit::RATE_LIMIT)
{
//top in board
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_RATE',
null,
self::HELPDESK_CODES['RATE'],
return $this->createLimitResult(
LimitType::Rate,
true,
self::buildLimitMessage(MessageCode::Rate, null, HelpdeskCode::Rate)
);
}
@@ -321,16 +398,18 @@ class RequestLimiter
if (Loader::includeModule('baas') && Baas::getInstance()->isAvailable())
{
//right 1 in board
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_PROMO',
self::SLIDER_CODES['BOOST_COPILOT_BOX'],
return $this->createLimitResult(
LimitType::Promo,
true,
self::buildLimitMessage(MessageCode::Promo, SliderCode::BoostCopilotBox)
);
}
//right 3 in board
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_PROMO',
self::SLIDER_CODES['BOX'],
return $this->createLimitResult(
LimitType::Promo,
true,
self::buildLimitMessage(MessageCode::Promo, SliderCode::Box)
);
}
@@ -338,57 +417,78 @@ class RequestLimiter
if ($typeLimit === TypeLimit::BAAS)
{
//right 2 in board
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_BAAS',
self::SLIDER_CODES['BOOST_COPILOT_BOX'],
return $this->createLimitResult(
LimitType::Baas,
true,
self::buildLimitMessage(MessageCode::Baas, SliderCode::BoostCopilotBox)
);
}
if ($limitError === ErrorLimit::PROMO_LIMIT)
{
//left 1 in board
return self::getLimitMessage(
'LANDING_REQUEST_LIMITER_ERROR_MONTHLY',
self::SLIDER_CODES['REQUEST_BOX'],
return $this->createLimitResult(
LimitType::Monthly,
true,
self::buildLimitMessage(MessageCode::Monthly, SliderCode::RequestBox)
);
}
return null;
return $this->createLimitResult(LimitType::None, false);
}
/**
* Returns the final text phrase with substituted links
* Creates a LimitCheckResult object.
*
* @param string $phraseCode Localization phrase code
* @param string|null $featurePromoterCode Feature promoter code (FEATURE_PROMOTER)
* @param string|null $helpdeskCode Helpdesk promoter code
* @param LimitType $limitType Type of the limit that was exceeded, or null if not applicable.
* @param bool $isExceeded True if the limit is exceeded, false otherwise.
* @param string|null $message Localized message for the user, or null if no limit is exceeded.
*
* @return string
* @return LimitCheckResult
*/
private static function getLimitMessage(
string $phraseCode,
?string $featurePromoterCode = null,
?string $helpdeskCode = null,
private function createLimitResult(LimitType $limitType, bool $isExceeded, ?string $message = null): LimitCheckResult
{
if ($message === null)
{
return new LimitCheckResult($limitType, $isExceeded);
}
return new LimitCheckResult($limitType, $isExceeded, $message);
}
/**
* Returns the final localized message with substituted links for limits.
*
* @param MessageCode $phraseCode Localization phrase code.
* @param SliderCode|null $featurePromoterCode Optional feature promoter code for link substitution.
* @param HelpdeskCode|null $helpdeskCode Optional helpdesk code for help link substitution.
*
* @return string Localized message with links.
*/
private static function buildLimitMessage(
MessageCode $phraseCode,
?SliderCode $featurePromoterCode = null,
?HelpdeskCode $helpdeskCode = null,
): string
{
if ($featurePromoterCode !== null)
{
return Loc::getMessage($phraseCode, [
'#LINK#' => "[url=/?FEATURE_PROMOTER={$featurePromoterCode}]",
'#/LINK#' => '[/url]'
return Loc::getMessage($phraseCode->value, [
'#LINK#' => "[url=/?FEATURE_PROMOTER=$featurePromoterCode->value]",
'#/LINK#' => '[/url]',
]);
}
if ($helpdeskCode !== null)
{
$helpUrl = Util::getArticleUrlByCode($helpdeskCode);
$helpUrl = Util::getArticleUrlByCode($helpdeskCode->value);
return Loc::getMessage($phraseCode, [
return Loc::getMessage($phraseCode->value, [
'#HELP#' => "[url=$helpUrl]",
'#/HELP#' => '[/url]'
'#/HELP#' => '[/url]',
]);
}
return Loc::getMessage($phraseCode);
return Loc::getMessage($phraseCode->value);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Bitrix\Landing\Copilot\Connector\AI\Type;
enum ErrorCode: string
{
/** @see \Bitrix\AI\Engine::ERRORS (key 'LIMIT_IS_EXCEEDED') */
case LimitCloud = 'LIMIT_IS_EXCEEDED';
/** @see \Bitrix\AI\Engine\Cloud\CloudEngine::ERROR_CODE_LIMIT_BAAS */
case LimitBaasCloud = 'LIMIT_IS_EXCEEDED_BAAS';
case RateLimit = 'RATE_LIMIT';
case Daily = 'LIMIT_IS_EXCEEDED_DAILY';
case Monthly = 'LIMIT_IS_EXCEEDED_MONTHLY';
case BaasRateLimit = 'LIMIT_IS_EXCEEDED_BAAS_RATE_LIMIT';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Bitrix\Landing\Copilot\Connector\AI\Type;
enum HelpdeskCode: string
{
case Rate = '24736310';
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Bitrix\Landing\Copilot\Connector\AI\Type;
enum LimitType: string
{
case None = 'none';
case Baas = 'baas';
case Promo = 'promo';
case Daily = 'daily';
case Monthly = 'monthly';
case Rate = 'rate';
case Market = 'market';
case Unregistered = 'unregistered';
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Bitrix\Landing\Copilot\Connector\AI\Type;
enum MessageCode: string
{
case Baas = 'LANDING_REQUEST_LIMITER_ERROR_BAAS';
case Promo = 'LANDING_REQUEST_LIMITER_ERROR_PROMO';
case Daily = 'LANDING_REQUEST_LIMITER_ERROR_DAILY';
case Monthly = 'LANDING_REQUEST_LIMITER_ERROR_MONTHLY';
case Rate = 'LANDING_REQUEST_LIMITER_ERROR_RATE';
case CloudRegistration = 'LANDING_REQUEST_LIMITER_ERROR_CLOUD_REGISTRATION';
case Market = 'LANDING_REQUEST_LIMITER_ERROR_MARKET';
case BaasRateLimit = 'LANDING_REQUEST_LIMITER_ERROR_BAAS_RATE_LIMIT';
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Bitrix\Landing\Copilot\Connector\AI\Type;
enum PromoLimitCode: string
{
case Daily = 'Daily';
case Monthly = 'Monthly';
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Bitrix\Landing\Copilot\Connector\AI\Type;
enum SliderCode: string
{
case BoostCopilot = 'limit_boost_copilot';
case Daily = 'limit_copilot_max_number_daily_requests';
case Monthly = 'limit_copilot_requests';
case BoostCopilotBox = 'limit_boost_copilot_box';
case RequestBox = 'limit_copilot_requests_box';
case Box = 'limit_copilot_box';
case Market = 'limit_subscription_market_access_buy_marketplus';
}

View File

@@ -12,6 +12,7 @@ use Bitrix\AI\Chatbot\Message\SystemMessage;
use Bitrix\Landing\Copilot\Data\Wishes;
use Bitrix\Landing\Copilot\Generation;
use Bitrix\Landing\Copilot\Data;
use Bitrix\Landing\Metrika;
use Bitrix\Main\LoaderException;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\SystemException;
@@ -162,6 +163,13 @@ class CreateSiteChatBot extends CopilotChatBot
->setScenario(new Generation\Scenario\CreateSite())
->setChatId($chatId)
->setWishes($wishes)
->setMetrikaFields(new Metrika\FieldsDto(
params: [[
3,
'context',
$wishes->isDemoWishes() ? 'no' : 'user'
]],
))
->execute()
)
{

View File

@@ -12,6 +12,8 @@ class Wishes
private array $wishes = [];
private ?string $company = null;
protected bool $isDemo = false;
/**
* Replace all wishes to passed array. Empty strings will be skipped
* @param array $wishes
@@ -48,10 +50,20 @@ class Wishes
public function setDemoWishes(): self
{
$this->wishes = [self::getDemoWish()];
$this->isDemo = true;
return $this;
}
/**
* Check is demo wishes used
* @return bool
*/
public function isDemoWishes(): bool
{
return $this->isDemo;
}
private static function getDemoWish(): string
{
return Loc::getMessage('LANDING_COPILOT_DEMO_WISH_' . (rand(1, 20)));

View File

@@ -10,7 +10,9 @@ use Bitrix\Landing\Copilot\Generation\GenerationException;
use Bitrix\Landing\Copilot\Generation\Scenario\IScenario;
use Bitrix\Landing\Copilot\Generation\Scenario\Scenarist;
use Bitrix\Landing\Copilot\Generation\Timer;
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
use Bitrix\Landing\Copilot\Model\GenerationsTable;
use Bitrix\Landing\Metrika;
use Bitrix\Main\Application;
use Bitrix\Main\ArgumentException;
use Bitrix\Main\ORM\Query\Query;
@@ -19,6 +21,7 @@ use Bitrix\Main\Web\Json;
/**
* This class is responsible for generating site content using various managers and connectors.
* It manages the generation process, scenario execution, metrika analytics, and error handling.
*/
class Generation
{
@@ -28,6 +31,8 @@ class Generation
private const EVENT_GENERATION_ERROR = 'onGenerationError';
private const EVENT_GENERATION_FINISH = 'onGenerationFinish';
private const DATA_METRIKA_KEY = 'metrikaFields';
private int $id;
private ?int $chatId = null;
private IScenario $scenario;
@@ -38,18 +43,28 @@ class Generation
private Scenarist $scenarist;
private Timer $timer;
protected ?Generation\Event $event = null;
private ?Generation\Event $event = null;
private ?Metrika\FieldsDto $metrikaFields = null;
private Metrika\MetrikaProviderParamService $metrikaProviderParamService;
/**
* Generation constructor.
* Initializes authorId, siteData, and timer.
*/
public function __construct()
{
$this->authorId = Landing\Manager::getUserId();
$this->siteData = new Data\Site();
$this->timer = new Timer();
$this->metrikaProviderParamService = new Metrika\MetrikaProviderParamService();
}
/**
* ID of user who created the site
* Returns the ID of the user who created the site.
*
* @return int
*/
public function getAuthorId(): int
@@ -58,8 +73,10 @@ class Generation
}
/**
* If generation start by chat - set ID
* @param int $chatId
* Sets the chat ID if generation is started by chat.
*
* @param int $chatId Chat ID.
*
* @return Generation
*/
public function setChatId(int $chatId): self
@@ -73,7 +90,8 @@ class Generation
}
/**
* If generation started by chat - get ID
* Gets the chat ID if generation is started by chat.
*
* @return int|null
*/
public function getChatId(): ?int
@@ -82,7 +100,10 @@ class Generation
}
/**
* Sets the scenario for this generation.
*
* @param IScenario $scenario
*
* @return Generation
*/
public function setScenario(IScenario $scenario): self
@@ -93,6 +114,8 @@ class Generation
}
/**
* Gets the scenario for this generation.
*
* @return IScenario|null
*/
public function getScenario(): ?IScenario
@@ -100,6 +123,13 @@ class Generation
return $this->scenario ?? null;
}
/**
* Sets the site data for this generation.
*
* @param Data\Site $siteData
*
* @return Generation
*/
public function setSiteData(Data\Site $siteData): self
{
$this->siteData = $siteData;
@@ -107,15 +137,22 @@ class Generation
return $this;
}
/**
* Gets the site data for this generation.
*
* @return Data\Site
*/
public function getSiteData(): Data\Site
{
return $this->siteData;
}
/**
* Get custom data for generations
* @param string|null $key
* @return mixed|null - null if key not set
* Gets custom data for this generation.
*
* @param string|null $key Optional key to retrieve a specific value.
*
* @return mixed|null Returns the value for the given key, all data if key is null, or null if not set.
*/
public function getData(?string $key = null): mixed
{
@@ -128,9 +165,11 @@ class Generation
}
/**
* Set custom data for generations
* @param string $key
* @param mixed $data
* Sets custom data for this generation.
*
* @param string $key Data key.
* @param mixed $data Data value.
*
* @return void
*/
public function setData(string $key, mixed $data): void
@@ -144,8 +183,10 @@ class Generation
}
/**
* Delete one key from data
* @param string $key
* Deletes a key from the custom data.
*
* @param string $key Data key to delete.
*
* @return void
*/
public function deleteData(string $key): void
@@ -156,6 +197,13 @@ class Generation
}
}
/**
* Sets the wishes for the site data.
*
* @param Data\Wishes $wishes
*
* @return Generation
*/
public function setWishes(Data\Wishes $wishes): self
{
$this->siteData->setWishes($wishes);
@@ -163,27 +211,118 @@ class Generation
return $this;
}
/**
* Gets the generation ID.
*
* @return int|null
*/
public function getId(): ?int
{
return $this->id ?? null;
}
/**
* Gets the current step of the generation.
*
* @return int|null
*/
public function getStep(): ?int
{
return $this->step ?? null;
}
/**
* Gets the timer object for this generation.
*
* @return Timer
*/
public function getTimer(): Timer
{
return $this->timer;
}
/**
* Try to find exists generation and init current by them data
* Sets metrika fields for analytics and stores them in custom data.
*
* @param int $generationId
* @param Metrika\FieldsDto $fields
*
* @return bool
* @return Generation
*/
public function setMetrikaFields(Metrika\FieldsDto $fields): self
{
$this->metrikaFields = $fields;
$this->setData(self::DATA_METRIKA_KEY, $this->metrikaFields->toArray());
return $this;
}
/**
* Gets metrika fields for analytics.
*
* @return Metrika\FieldsDto|null
*/
private function getMetrikaFields(): ?Metrika\FieldsDto
{
$saved =
$this->getData(self::DATA_METRIKA_KEY)
? Metrika\FieldsDto::fromArray($this->getData(self::DATA_METRIKA_KEY))
: null
;
if (!$saved)
{
return $this->metrikaFields;
}
return $saved->addFields($this->metrikaFields);
}
/**
* Gets the Metrika analytics object for the given event.
*
* @param Metrika\Events $event
*
* @return Metrika\Metrika
*/
public function getMetrika(Metrika\Events $event): Metrika\Metrika
{
$metrika = new Metrika\Metrika(
$this->scenario->getAnalyticCategory(),
$event,
Metrika\Tools::ai
);
$this->metrikaProviderParamService->setParams($metrika, $event);
$fields = $this->getMetrikaFields();
if (isset($fields))
{
foreach ($fields->params ?? [] as $param)
{
if (count($param) === 3)
{
$metrika->setParam(
(int)$param[0],
(string)$param[1],
(string)$param[2],
);
}
}
if (isset($fields->subSection))
{
$metrika->setSubSection($this->metrikaFields->subSection);
}
}
return $metrika;
}
/**
* Tries to find an existing generation by ID and initializes this instance with its data.
*
* @param int $generationId Generation ID.
*
* @return bool True if found and initialized, false otherwise.
*/
public function initById(int $generationId): bool
{
@@ -201,11 +340,12 @@ class Generation
}
/**
* Try to find exists generation by site ID and init current by them data
* Tries to find an existing generation by site ID and scenario, and initializes this instance with its data.
*
* @param int $siteId
* @param IScenario $scenario
* @return bool
* @param int $siteId Site ID.
* @param IScenario $scenario Scenario instance.
*
* @return bool True if found and initialized, false otherwise.
*/
public function initBySiteId(int $siteId, IScenario $scenario): bool
{
@@ -218,6 +358,12 @@ class Generation
return $this->initExists($filter);
}
/**
* Initializes this instance with data from an existing generation matching the filter.
* @param Filter\ConditionTree $filter ORM filter for the query.
*
* @return bool True if found and initialized, false otherwise.
*/
private function initExists(Filter\ConditionTree $filter): bool
{
$generation = GenerationsTable::query()
@@ -275,8 +421,9 @@ class Generation
}
/**
* Run process.
* @return bool - false if error while executing
* Runs the generation process.
*
* @return bool False if an error occurs while executing, true otherwise.
*/
public function execute(): bool
{
@@ -319,6 +466,7 @@ class Generation
}
catch (GenerationException $e)
{
$this->sendMetrikaError($e);
$this->scenario->getChatbot()?->sendErrorMessage(new ChatBotMessageDto(
$this->getChatId() ?? 0,
$this->id,
@@ -351,6 +499,11 @@ class Generation
return true;
}
/**
* Checks if the generation is executable (all required properties are set).
*
* @return bool
*/
private function isExecutable(): bool
{
return isset(
@@ -360,23 +513,39 @@ class Generation
);
}
/**
* Gets the lock name for this generation.
*
* @return string
*/
private function getLockName(): string
{
return 'landing_copilot_generation_' . ($this->id ?? 0);
}
/**
* Schedules an agent to retry execution.
*
* @return void
*/
private function setAgent(): void
{
Agent::addUniqueAgent('executeGeneration', [$this->id], 60, 10);
}
/**
* Removes the scheduled agent for this generation.
*
* @return void
*/
private function deleteAgent(): void
{
Agent::deleteUniqueAgent('executeGeneration', [$this->id]);
}
/**
* Finish all generation processed, mark as completed
* Finishes the generation process and marks it as completed.
*
* @return void
*/
public function finish(): void
@@ -391,7 +560,8 @@ class Generation
}
/**
* Check if scenario execute all steps
* Checks if the scenario has executed all steps.
*
* @return bool
*/
public function isFinished(): bool
@@ -404,13 +574,19 @@ class Generation
return $this->scenarist->isFinished();
}
/**
* Handles actions to perform when the generation is finished.
*
* @return void
*/
private function onFinish(): void
{
$this->getEvent()->send(self::EVENT_GENERATION_FINISH);
}
/**
* Check if at least one scenario step has error and not execute
* Checks if at least one scenario step has an error and was not executed.
*
* @return bool
*/
public function isError(): bool
@@ -424,7 +600,8 @@ class Generation
}
/**
* Prepare scenario to restart generation after error
* Prepares the scenario to restart generation after an error by clearing errors.
*
* @return $this
*/
public function clearErrors(): self
@@ -437,6 +614,11 @@ class Generation
return $this;
}
/**
* Initializes the Scenarist object for this generation.
*
* @return bool True if successfully initialized, false otherwise.
*/
private function initScenarist(): bool
{
if (!isset(
@@ -459,6 +641,11 @@ class Generation
return true;
}
/**
* Saves the current generation state to the database.
*
* @return bool True on success, false otherwise.
*/
private function save(): bool
{
if (!isset(
@@ -503,7 +690,8 @@ class Generation
}
/**
* Get object for send front and backend events
* Gets the event object for sending front-end and back-end events.
*
* @return Generation\Event
*/
public function getEvent(): Generation\Event
@@ -524,6 +712,13 @@ class Generation
return $this->event;
}
/**
* Checks if a generation with the given ID exists.
*
* @param int $id Generation ID.
*
* @return bool True if exists, false otherwise.
*/
public static function checkExists(int $id): bool
{
static $generations = [];
@@ -540,6 +735,13 @@ class Generation
return $generations[$id];
}
/**
* Gets block data for the given blocks as a JSON string.
*
* @param array $blocks Array of block objects.
*
* @return string JSON-encoded block data, or empty string on error.
*/
public function getBlocksData(array $blocks): string
{
$siteData = $this->getSiteData();
@@ -566,4 +768,52 @@ class Generation
return $blockDataEncoded;
}
/**
* Sends metrika analytics for an error during generation.
*
* @param GenerationException $e
*
* @return void
*/
private function sendMetrikaError(GenerationException $e): void
{
$errorCode = $e->getErrorCode();
/**
* @var Metrika\Statuses $status
*/
$status = match ($errorCode)
{
GenerationErrors::requestQuotaExceeded => $e->getParams()['metrikaStatus'] ?? Metrika\Statuses::ErrorB24,
GenerationErrors::restrictedRequest => Metrika\Statuses::ErrorContentPolicy,
GenerationErrors::notExistResponse,
GenerationErrors::notFullyResponse,
GenerationErrors::notCorrectResponse => Metrika\Statuses::ErrorProvider,
default => Metrika\Statuses::ErrorB24,
};
$scenario = $this->getScenario();
if ($scenario)
{
$stepsMap = $scenario->getMap();
if (isset($stepsMap[$this->step]))
{
$step = $stepsMap[$this->step];
}
}
if (isset($step))
{
$analyticEvent = $step->getAnalyticEvent();
}
if (!isset($analyticEvent))
{
$analyticEvent = Metrika\Events::unknown;
}
$metrika = $this->getMetrika($analyticEvent);
$metrika->setStatus($status);
$metrika->send();
}
}

View File

@@ -7,52 +7,75 @@ namespace Bitrix\Landing\Copilot\Generation;
use Bitrix\Main\SystemException;
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
/**
* Exception class for errors occurring during AI generation processes.
* Stores an error code, optional additional message, and parameters for detailed context.
*/
class GenerationException extends SystemException
{
/**
* Array with error message templates.
* @var array Parameters for the error message template.
*/
private ?array $params;
private array $params;
/**
* Exception constructor.
*
* @param GenerationErrors $code Error code.
* @param string $message Additional message (optional).
* @var GenerationErrors Error code enum for this exception.
*/
public function __construct(GenerationErrors $code, string $message = '', ?array $params = null)
private GenerationErrors $errorCode;
/**
* GenerationException constructor.
*
* @param GenerationErrors $errorCode Error code enum describing the type of generation error.
* @param string $message Optional additional message for more context.
* @param array $params Optional parameters for the error message template.
*/
public function __construct(GenerationErrors $errorCode, string $message = '', array $params = [])
{
$this->params = $params;
$errorMessage = $this->buildErrorMessage($code, $message);
$this->errorCode = $errorCode;
$errorMessage = $this->buildErrorMessage($errorCode, $message);
parent::__construct($errorMessage, $code->value);
parent::__construct($errorMessage, $errorCode->value);
}
/**
* Get exception code transform to enum
* @return ?GenerationErrors
* Returns the exception code as a GenerationErrors enum instance.
*
* @return GenerationErrors The error code as an enum value.
*/
public function getCodeObject(): ?GenerationErrors
public function getCodeObject(): GenerationErrors
{
return GenerationErrors::tryFrom($this->getCode()) ?? GenerationErrors::default;
}
/**
* @return array|null
* Returns the parameters associated with this exception.
*
* @return array Parameters for the error message template.
*/
public function getParams(): ?array
public function getParams(): array
{
return $this->params;
}
/**
* Receives an error message based on the code.
* Returns the error code enum for this exception.
*
* @param GenerationErrors $code Error code DTO
* @param string $additionalMessage Additional message.
* @return GenerationErrors The error code.
*/
public function getErrorCode(): GenerationErrors
{
return $this->errorCode;
}
/**
* Builds the final error message based on the code and an optional additional message.
*
* @return string Error message.
* @param GenerationErrors $code Error code enum.
* @param string $additionalMessage Optional additional message for more context.
*
* @return string The complete error message.
*/
protected function buildErrorMessage(GenerationErrors $code, string $additionalMessage): string
{
@@ -66,6 +89,13 @@ class GenerationException extends SystemException
return $message . '.';
}
/**
* Returns a default error message for a given error code.
*
* @param GenerationErrors $code Error code enum.
*
* @return string The default error message for the code.
*/
private static function getErrorMessage(GenerationErrors $code): string
{
$messages = [

View File

@@ -20,6 +20,12 @@ use Bitrix\Main\Type\DateTime;
use Bitrix\Main\Web;
use Exception;
/**
* Class Request
*
* Handles the lifecycle of a single AI request within a generation step,
* including sending, result/error processing, persistence, and status management.
*/
class Request
{
// todo: get individual time from step
@@ -39,6 +45,12 @@ class Request
private Type\RequestStatus $status = Type\RequestStatus::New;
private RequestLimiter $requestLimiter;
/**
* Request constructor.
*
* @param int $generationId The ID of the generation this request belongs to.
* @param int $stepId The step ID within the generation.
*/
public function __construct(int $generationId, int $stepId)
{
$this->generationId = $generationId;
@@ -46,11 +58,14 @@ class Request
}
/**
* Send request to AI provider
* @param Prompt $prompt
* @param IConnector $connector
* @return bool
* @throws GenerationException
* Sends a request to the AI provider using the given prompt and connector.
*
* @param Prompt $prompt The prompt to send.
* @param IConnector $connector The AI connector to use.
*
* @return bool True on successful send and save, false otherwise.
*
* @throws GenerationException If the request fails or a quota/limit is exceeded.
*/
public function send(Prompt $prompt, IConnector $connector): bool
{
@@ -115,9 +130,13 @@ class Request
}
/**
* @param Main\Error|null $error
* Processes an error from the AI provider and throws a GenerationException.
*
* @param Main\Error|null $error The error object from the provider.
*
* @return void
* @throws GenerationException
*
* @throws GenerationException Always throws; either for quota exceeded or generic request error.
*/
private function processError(?Main\Error $error): void
{
@@ -126,18 +145,23 @@ class Request
throw new GenerationException(GenerationErrors::notSendRequest);
}
$errorText = $this->getRequestLimiter()->getTextFromError($error);
if ($errorText)
if ($this->getRequestLimiter()->checkError($error))
{
$params = [
'errorText' => $errorText,
];
throw new GenerationException(GenerationErrors::requestQuotaExceeded, $error->getMessage(), $params);
$message = $this->getRequestLimiter()->getCheckResultMessage();
if (is_string($message))
{
throw new GenerationException(
GenerationErrors::requestQuotaExceeded,
$error->getMessage(),
[
'errorText' => $message,
'metrikaStatus' => $this->getRequestLimiter()->getCheckResult()?->getMetrikaStatus(),
]
);
}
}
throw new GenerationException(GenerationErrors::errorInRequest, $error->getMessage(), null);
throw new GenerationException(GenerationErrors::errorInRequest, $error->getMessage());
}
/**
@@ -155,6 +179,11 @@ class Request
return $this->requestLimiter;
}
/**
* Marks the request as applied to the step, updating the status and persistence.
*
* @return bool True if successfully applied, false otherwise.
*/
public function setApplied(): bool
{
if ($this->status->value < Type\RequestStatus::Received->value)
@@ -186,9 +215,11 @@ class Request
}
/**
* Save result of AI request
* @param array $result - data array
* @return bool
* Saves the result of the AI request.
*
* @param array $result The result data array.
*
* @return bool True on success, false otherwise.
*/
public function saveResult(array $result): bool
{
@@ -205,9 +236,11 @@ class Request
}
/**
* Save error code and message
* @param Generation\Error $error
* @return bool
* Saves an error code and message for this request.
*
* @param Generation\Error $error The error DTO.
*
* @return bool True on success, false otherwise.
*/
public function saveError(Generation\Error $error): bool
{
@@ -223,12 +256,22 @@ class Request
return $this->save();
}
/**
* Marks this request as deleted and persists the change.
*
* @return void
*/
public function setDeleted(): void
{
$this->isDeleted = true;
$this->save();
}
/**
* Saves the current state of the request to the database.
*
* @return bool True on success, false otherwise.
*/
private function save(): bool
{
if ($this->status->value < Type\RequestStatus::Sent->value)
@@ -286,8 +329,9 @@ class Request
}
/**
* Return ID of current generation
* @return int
* Returns the ID of the current generation.
*
* @return int Generation ID.
*/
public function getGenerationId(): int
{
@@ -295,8 +339,9 @@ class Request
}
/**
* If request received - return result data. Else - return null
* @return array|null
* Returns the result data if the request was received, or null otherwise.
*
* @return array|null Result data array or null.
*/
public function getResult(): ?array
{
@@ -304,8 +349,9 @@ class Request
}
/**
* If request finish with error - get error DTO
* @return ?Generation\Error
* Returns the error DTO if the request finished with an error, or null otherwise.
*
* @return Generation\Error|null Error DTO or null.
*/
public function getError(): ?Generation\Error
{
@@ -313,7 +359,8 @@ class Request
}
/**
* If request receive answer
* Returns true if the request has received an answer (result or error).
*
* @return bool
*/
public function isReceived(): bool
@@ -322,7 +369,8 @@ class Request
}
/**
* If request answer was applied to step
* Returns true if the request answer was applied to the step.
*
* @return bool
*/
public function isApplied(): bool
@@ -331,8 +379,9 @@ class Request
}
/**
* ID in DB
* @return int|null
* Returns the database ID of this request.
*
* @return int|null Request ID or null if not persisted.
*/
public function getId(): ?int
{
@@ -340,9 +389,12 @@ class Request
}
/**
* @param int $generationId
* @param int $stepId
* @return array<Request> - array of exists requests
* Returns all existing requests for a given generation and step.
*
* @param int $generationId Generation ID.
* @param int $stepId Step ID.
*
* @return Request[] Array of existing Request objects.
*/
public static function getByGeneration(int $generationId, int $stepId): array
{
@@ -356,6 +408,13 @@ class Request
return self::getExists($filter);
}
/**
* Returns the request by its hash, or null if not found.
*
* @param string $hash Request hash.
*
* @return Request|null The found Request object or null.
*/
public static function getByHash(string $hash): ?self
{
$filter =
@@ -368,6 +427,13 @@ class Request
return array_shift($exists);
}
/**
* Returns the request by its database ID, or null if not found.
*
* @param int $id Request ID.
*
* @return Request|null The found Request object or null.
*/
public static function getById(int $id): ?self
{
$filter =
@@ -381,8 +447,11 @@ class Request
}
/**
* @param Filter\ConditionTree $filter - ORM filter object
* @return Request[]
* Returns all existing requests matching the given ORM filter.
*
* @param Filter\ConditionTree $filter ORM filter object.
*
* @return Request[] Array of Request objects.
*/
private static function getExists(Filter\ConditionTree $filter): array
{
@@ -431,6 +500,13 @@ class Request
return $exists;
}
/**
* Initializes the Request object from an EO_Requests entity.
*
* @param EO_Requests $request The ORM entity object.
*
* @return self The initialized Request object.
*/
private function initByEntity(EO_Requests $request): self
{
$this->id = $request->getId();
@@ -477,6 +553,11 @@ class Request
return $this;
}
/**
* Checks if the request has exceeded the maximum expected time without a result or error.
*
* @return bool True if time is over, false otherwise.
*/
private function isTimeIsOver(): bool
{
if (

View File

@@ -3,6 +3,8 @@ declare(strict_types=1);
namespace Bitrix\Landing\Copilot\Generation\Scenario;
use Bitrix\Landing\Metrika;
abstract class BaseScenario implements IScenario
{
/**
@@ -12,6 +14,13 @@ abstract class BaseScenario implements IScenario
*/
abstract public function getQuotaCalculateStep(): int;
abstract public function getAnalyticCategory(): Metrika\Categories;
public function getAsyncRelations(): ?array
{
return null;
}
/**
* @inheritdoc
*/

View File

@@ -7,6 +7,7 @@ use Bitrix\Landing\Copilot\Connector;
use Bitrix\Landing\Copilot\Connector\Chat\ICopilotChatBot;
use Bitrix\Landing\Copilot\Generation;
use Bitrix\Landing\Copilot\Generation\Step;
use Bitrix\Landing\Metrika;
class ChangeBlock extends BaseScenario
{
@@ -45,6 +46,20 @@ class ChangeBlock extends BaseScenario
return 10;
}
public function getAnalyticCategory(): Metrika\Categories
{
return Metrika\Categories::BlockEdition;
}
public function getAsyncRelations(): ?array
{
return [
30 => [
40,
],
];
}
/**
* @inheritdoc
*/

View File

@@ -47,6 +47,11 @@ class CreateSite extends BaseScenario
return 20;
}
public function getAnalyticCategory(): Metrika\Categories
{
return Metrika\Categories::SiteGeneration;
}
/**
* @inheritdoc
*/

View File

@@ -6,6 +6,7 @@ namespace Bitrix\Landing\Copilot\Generation\Scenario;
use Bitrix\Landing\Copilot\Connector\Chat\ICopilotChatBot;
use Bitrix\Landing\Copilot\Generation\Step\IStep;
use Bitrix\Landing\Copilot\Generation;
use Bitrix\Landing\Metrika;
interface IScenario
{
@@ -21,6 +22,15 @@ interface IScenario
*/
public function getChatbot(): ?ICopilotChatBot;
public function getAnalyticCategory(): Metrika\Categories;
/**
* If some steps must be run only after async step - need set relations
* f.e. [asyncStepId => [dependent_step_ids]]
* @return array|null
*/
public function getAsyncRelations(): ?array;
/**
* Get ID of first step in scenario
* @return int|null - if null - has no steps in map

View File

@@ -3,19 +3,26 @@ declare(strict_types=1);
namespace Bitrix\Landing\Copilot\Generation\Scenario;
use Bitrix\AI\Limiter\Enums\TypeLimit;
use Bitrix\Landing\Copilot\Connector;
use Bitrix\Landing\Copilot\Connector\AI\RequestLimiter;
use Bitrix\Landing\Copilot\Connector\Chat\ChatBotMessageDto;
use Bitrix\Landing\Copilot\Generation;
use Bitrix\Landing\Copilot\Generation\GenerationException;
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
use Bitrix\Landing\Copilot\Generation\GenerationException;
use Bitrix\Landing\Copilot\Generation\Type\RequestQuotaDto;
use Bitrix\Landing\Copilot\Generation\Type\ScenarioStepDto;
use Bitrix\Landing\Copilot\Generation\Type\StepStatuses;
use Bitrix\Landing\Copilot\Model\StepsTable;
use Bitrix\Landing\Metrika;
use Bitrix\Main\Loader;
/**
* Class Scenarist
*
* Manages the execution flow of a scenario consisting of multiple steps for AI-powered content generation.
* Handles step status tracking, error handling, quota checks, analytics, and callback hooks for step changes and
* completion.
*/
class Scenarist
{
private const EVENT_STEP = 'onExecuteStep';
@@ -26,27 +33,33 @@ class Scenarist
private ?int $stepId;
/**
* @var ScenarioStepDto[]
* @var ScenarioStepDto[] Array of scenario steps indexed by step ID.
*/
private array $steps;
/**
* @var callable - call when stepId changed and must be saved
* @var callable|null Callback invoked when the step ID changes and must be saved.
*/
private $onStepChangeCallback;
/**
* @var callable - call when site data changed and must be saved
* @var callable|null Callback invoked when site data changes and must be saved.
*/
private $onSaveCallback;
/**
* @var callable - call when scenario finished
* @var callable|null Callback invoked when the scenario is finished.
*/
private $onFinishCallback;
private RequestLimiter $requestLimiter;
/**
* Scenarist constructor.
*
* @param IScenario $scenario The scenario instance to execute.
* @param Generation $generation The generation context.
*/
public function __construct(IScenario $scenario, Generation $generation)
{
$this->scenario = $scenario;
@@ -57,6 +70,11 @@ class Scenarist
$this->initSteps();
}
/**
* Initializes the steps array from the scenario map and loads persisted step statuses.
*
* @return void
*/
private function initSteps(): void
{
foreach ($this->scenario->getMap() as $stepId => $step)
@@ -88,14 +106,20 @@ class Scenarist
}
}
/**
* Returns the current step ID.
*
* @return int|null
*/
public function getStep(): ?int
{
return $this->stepId;
}
/**
* Check if scenario execute all steps
* @return bool
* Checks if all scenario steps are finished.
*
* @return bool True if all steps are finished, false otherwise.
*/
public function isFinished(): bool
{
@@ -111,7 +135,8 @@ class Scenarist
}
/**
* Mark all scenario steps as completed
* Marks all scenario steps as finished.
*
* @return void
*/
public function finish(): void
@@ -126,8 +151,9 @@ class Scenarist
}
/**
* Check if at least one scenario step has error and not execute
* @return bool
* Checks if at least one scenario step has an error and is not executed.
*
* @return bool True if any step is in error state, false otherwise.
*/
public function isError(): bool
{
@@ -143,7 +169,8 @@ class Scenarist
}
/**
* Prepare scenario to restart generation after error.
* Prepares the scenario to restart generation after an error by clearing errors in all steps.
*
* @return void
*/
public function clearErrors(): void
@@ -162,6 +189,13 @@ class Scenarist
}
}
/**
* Sets a callback to be called when the step ID changes.
*
* @param callable $callback
*
* @return $this
*/
public function onStepChange(callable $callback): self
{
$this->onStepChangeCallback = $callback;
@@ -169,6 +203,11 @@ class Scenarist
return $this;
}
/**
* Invokes the step change callback if set.
*
* @return void
*/
private function callOnStepChange(): void
{
if (isset($this->onStepChangeCallback) && is_int($this->stepId))
@@ -177,6 +216,13 @@ class Scenarist
}
}
/**
* Sets a callback to be called when the scenario state should be saved.
*
* @param callable $callback
*
* @return $this
*/
public function onSave(callable $callback): self
{
$this->onSaveCallback = $callback;
@@ -184,6 +230,11 @@ class Scenarist
return $this;
}
/**
* Invokes the save callback if set.
*
* @return void
*/
private function callOnSave(): void
{
if (isset($this->onSaveCallback))
@@ -192,6 +243,13 @@ class Scenarist
}
}
/**
* Sets a callback to be called when the scenario is finished.
*
* @param callable $callback
*
* @return $this
*/
public function onFinish(callable $callback): self
{
$this->onFinishCallback = $callback;
@@ -199,6 +257,11 @@ class Scenarist
return $this;
}
/**
* Invokes the finish callback if set.
*
* @return void
*/
private function callOnFinish(): void
{
if (isset($this->onFinishCallback))
@@ -208,9 +271,11 @@ class Scenarist
}
/**
* Run scenario
* Executes the scenario, running steps in order and handling errors, quotas, and analytics.
*
* @return void
* @throws GenerationException
*
* @throws GenerationException If a step fails or a quota is exceeded.
*/
public function execute(): void
{
@@ -225,6 +290,11 @@ class Scenarist
return;
}
if ($this->stepId === $this->scenario->getFirstStepId())
{
$this->sendMetrikaStart();
}
foreach ($this->steps as $stepId => $step)
{
if ($stepId > $this->stepId)
@@ -232,10 +302,7 @@ class Scenarist
break;
}
if (
$step->status === StepStatuses::Finished
|| $step->status === StepStatuses::Error
)
if (!$this->isNeedExecuteStep($step))
{
continue;
}
@@ -251,14 +318,11 @@ class Scenarist
}
if (
$this->stepId === $stepId
&& (
$step->step->isFinished()
|| $step->step->isAsync()
)
$step->step->isFinished()
|| $step->step->isAsync()
)
{
$this->stepId = $this->scenario->getNextStepId($this->stepId);
$this->stepId = $this->getNextStep($stepId);
if (!$this->stepId)
{
break;
@@ -276,11 +340,69 @@ class Scenarist
}
}
private function isNeedExecuteStep(ScenarioStepDto $step): bool
{
if (
$step->status === StepStatuses::Finished
|| $step->status === StepStatuses::Error
)
{
return false;
}
$relations = $this->scenario->getAsyncRelations();
if ($relations)
{
foreach ($relations as $parent => $dependents)
{
if (
in_array($step->stepId, $dependents, true)
&& !$this->steps[$parent]?->step->isFinished()
)
{
return false;
}
}
}
return true;
}
private function getNextStep(int $stepId): ?int
{
$scenario = $this->scenario;
$recursiveGetNext = function ($currentId) use ($scenario, &$recursiveGetNext)
{
$nextId = $scenario->getNextStepId($currentId);
$relations = $scenario->getAsyncRelations();
if ($relations)
{
foreach ($relations as $parent => $dependents)
{
if (
in_array($nextId, $dependents, true)
&& !$this->steps[$parent]?->step->isFinished()
)
{
return $recursiveGetNext($nextId);
}
}
}
return $nextId;
};
return $recursiveGetNext($stepId);
}
/**
* Find current step and run them
* @param ScenarioStepDto $step
* Executes a single scenario step, handling quota checks and status updates.
*
* @param ScenarioStepDto $step The step to execute.
*
* @return void
* @throws GenerationException
*
* @throws GenerationException If the step fails or a quota is exceeded.
*/
private function executeStep(ScenarioStepDto $step): void
{
@@ -289,9 +411,22 @@ class Scenarist
if (
!$step->step->isStarted()
&& $step->stepId === $this->scenario->getQuotaCalculateStep()
&& $this->checkRequestQuotas()
)
{
$this->checkRequestQuotas();
$requestLimiter = $this->getRequestLimiter();
$message = $requestLimiter->getCheckResultMessage();
if (is_string($message))
{
throw new GenerationException(
GenerationErrors::requestQuotaExceeded,
$message,
[
'errorText' => $message,
'metrikaStatus' => $requestLimiter->getCheckResult()?->getMetrikaStatus(),
]
);
}
}
$step->step->execute();
@@ -304,6 +439,10 @@ class Scenarist
if ($step->step->isFinished())
{
$this->saveStepStatus($step, StepStatuses::Finished);
if ($step->step->isChanged())
{
$this->sendMetrikaStepSuccess($step);
}
}
if ($step->step->isChanged() || $step->step->isFinished())
@@ -314,6 +453,14 @@ class Scenarist
$this->generation->getEvent()->send(self::EVENT_STEP);
}
/**
* Saves the status of a scenario step to the database.
*
* @param ScenarioStepDto $step The step whose status is being saved.
* @param StepStatuses $status The new status.
*
* @return void
*/
private function saveStepStatus(ScenarioStepDto $step, StepStatuses $status): void
{
$step->status = $status;
@@ -341,21 +488,15 @@ class Scenarist
}
/**
* @return void
* @throws GenerationException
* Checks if the request quota for the scenario is exceeded.
*
* @return bool True if the request quota is exceeded, false otherwise.
*/
private function checkRequestQuotas(): void
private function checkRequestQuotas(): bool
{
$quotaLimitText = $this->getQuotaLimitText();
if (is_string($quotaLimitText))
if (!Loader::includeModule('ai'))
{
throw new GenerationException(
GenerationErrors::requestQuotaExceeded,
$quotaLimitText,
[
'errorText' => $quotaLimitText,
]
);
return false;
}
if (
@@ -363,37 +504,30 @@ class Scenarist
|| $this->generation->getChatId() <= 0
)
{
return;
return false;
}
$this->generation->getScenario()?->getChatbot()?->onRequestQuotaOk(
new ChatBotMessageDto(
$this->generation->getChatId(),
$this->generation->getId(),
)
);
}
$requestLimiter = $this->getRequestLimiter();
private function getQuotaLimitText(): ?string
{
if (!Loader::includeModule('ai'))
$isRequestQuotaExceeded = true;
if (!$requestLimiter->checkQuota($this->getRequestQuotasSum()))
{
return null;
$isRequestQuotaExceeded = false;
$this->generation->getScenario()?->getChatbot()?->onRequestQuotaOk(
new ChatBotMessageDto(
$this->generation->getChatId(),
$this->generation->getId(),
)
);
}
$requestCount = $this->getRequestQuotasSum();
if ($requestCount <= 0)
{
return null;
}
return $this->getRequestLimiter()->getTextFromCheckLimit($requestCount);
return $isRequestQuotaExceeded;
}
/**
* Return sum of request limits by all steps
* Returns an array of request quotas for all steps in the scenario.
*
* @return RequestQuotaDto[]
* @return RequestQuotaDto[] Array of request quota DTOs.
*/
private function getRequestQuotas(): array
{
@@ -423,8 +557,9 @@ class Scenarist
}
/**
* Get sum of all request quotas, ignore types
* @return int
* Returns the sum of all request quotas, ignoring types.
*
* @return int Total request count.
*/
private function getRequestQuotasSum(): int
{
@@ -440,15 +575,43 @@ class Scenarist
/**
* Retrieves the RequestLimiter instance, initializing it if not already set.
*
* @return RequestLimiter The RequestLimiter instance.
* @return RequestLimiter
*/
private function getRequestLimiter(): RequestLimiter
{
if (empty($this->requestLimiter))
if (!isset($this->requestLimiter))
{
$this->requestLimiter = new RequestLimiter();
}
return $this->requestLimiter;
}
/**
* Sends a Metrika analytics event for the start of the scenario.
*
* @return void
*/
private function sendMetrikaStart(): void
{
$metrika = $this->generation->getMetrika(Metrika\Events::start);
$metrika->send();
}
/**
* Sends a Metrika analytics event for a successful step execution.
*
* @param ScenarioStepDto $step The step for which to send analytics.
*
* @return void
*/
private function sendMetrikaStepSuccess(ScenarioStepDto $step): void
{
$event = $step->step->getAnalyticEvent();
if (isset($event))
{
$metrika = $this->generation->getMetrika($event);
$metrika->send();
}
}
}

View File

@@ -7,6 +7,7 @@ use Bitrix\Landing\Copilot\Data;
use Bitrix\Landing\Copilot\Generation;
use Bitrix\Landing\Copilot\Generation\GenerationException;
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
use Bitrix\Landing\Metrika;
abstract class BaseStep implements IStep
{
@@ -98,4 +99,14 @@ abstract class BaseStep implements IStep
{
return $this->generation->getEvent();
}
/**
* Step may, or not may send analytic event.
* The Scenarist decides when to send the event.
* @return Metrika\Events|null
*/
public function getAnalyticEvent(): ?Metrika\Events
{
return null;
}
}

View File

@@ -7,6 +7,7 @@ use Bitrix\Landing\Copilot\Data\Site;
use Bitrix\Landing\Copilot\Generation;
use Bitrix\Landing\Copilot\Generation\GenerationException;
use Bitrix\Landing\Copilot\Generation\Type\RequestQuotaDto;
use Bitrix\Landing\Metrika;
interface IStep
{
@@ -31,7 +32,6 @@ interface IStep
*/
public function isAsync(): bool;
/**
* Check if step was start executing
* @return bool
@@ -64,4 +64,11 @@ interface IStep
* @return RequestQuotaDto|null
*/
public static function getRequestQuota(Site $siteData): ?RequestQuotaDto;
/**
* Step may, or not may send analytic event.
* The Scenarist decides when to send the event.
* @return Metrika\Events|null
*/
public function getAnalyticEvent(): ?Metrika\Events;
}

View File

@@ -6,7 +6,6 @@ namespace Bitrix\Landing\Copilot\Generation\Step;
use Bitrix\Landing\Copilot\Connector\AI;
use Bitrix\Landing\Copilot\Connector\AI\Prompt;
use Bitrix\Landing\Copilot\Converter;
use Bitrix\Landing\Copilot\Data\Block\Operator;
use Bitrix\Landing\Copilot\Data\Type\NodeType;
use Bitrix\Landing\Copilot\Generation\Error;
use Bitrix\Landing\Copilot\Generation\GenerationException;
@@ -15,6 +14,7 @@ use Bitrix\Landing\Copilot\Generation\Type\Errors;
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
use Bitrix\Landing\Copilot\Generation\Type\RequestQuotaDto;
use Bitrix\Landing\Copilot\Data\Site;
use Bitrix\Landing\Metrika;
use Bitrix\Landing;
use Bitrix\Landing\Rights;
@@ -45,6 +45,11 @@ class RequestBlockContent extends RequestSingle
return new RequestQuotaDto(self::getConnectorClass(), 1);
}
public function getAnalyticEvent(): ?Metrika\Events
{
return Metrika\Events::textsGeneration;
}
protected function getPrompt(): Prompt
{
$prompt = new Prompt('landing_ai_block_content');

View File

@@ -14,6 +14,7 @@ use Bitrix\Landing\Copilot\Generation\Type\RequestEntityDto;
use Bitrix\Landing\Copilot\Generation\Type\RequestQuotaDto;
use Bitrix\Landing\Copilot\Generation\Type\RequestEntities;
use Bitrix\Landing\Copilot\Model\RequestToEntitiesTable;
use Bitrix\Landing\Metrika;
use Bitrix\Main\ORM\Query\Query;
class RequestImages extends RequestMultiple
@@ -48,6 +49,11 @@ class RequestImages extends RequestMultiple
);
}
public function getAnalyticEvent(): ?Metrika\Events
{
return Metrika\Events::imagesGeneration;
}
/**
* Get not avatar images count, check all blocks and all nodes
*

View File

@@ -81,10 +81,7 @@ abstract class RequestMultiple extends AIStep
{
foreach ($this->getEntitiesToRequest() as $entity)
{
if (
isset($entity->requestId)
&& isset($this->requests[$entity->requestId])
)
if (isset($entity->requestId, $this->requests[$entity->requestId]))
{
return true;
}

View File

@@ -14,6 +14,7 @@ use Bitrix\Landing\Copilot\Generation\PromptGenerator;
use Bitrix\Landing\Copilot\Generation\PromptTemplateProvider;
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
use Bitrix\Landing\Copilot\Generation\Type\RequestQuotaDto;
use Bitrix\Landing\Metrika;
use Bitrix\Main\Web;
use Exception;
@@ -44,6 +45,11 @@ class RequestSiteContent extends RequestSingle
return new RequestQuotaDto(self::getConnectorClass(), 1);
}
public function getAnalyticEvent(): ?Metrika\Events
{
return Metrika\Events::textsGeneration;
}
/**
* @inheritdoc
*/
@@ -206,7 +212,7 @@ class RequestSiteContent extends RequestSingle
if ($countPromptTexts !== $countPlaceholders)
{
$diffCount = $countPlaceholders - $countPromptTexts;
$diffCount = $countPlaceholders - $countPromptTexts;
if ($diffCount > 0)
{
for ($i = 0; $i < $diffCount; $i++)

View File

@@ -12,6 +12,7 @@ use Bitrix\Landing\Copilot\Generation\Markers;
use Bitrix\Landing\Copilot\Generation\Type\Errors;
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
use Bitrix\Landing\Copilot\Generation\Type\RequestQuotaDto;
use Bitrix\Landing\Metrika;
class RequestSiteData extends RequestSingle
{
@@ -40,6 +41,11 @@ class RequestSiteData extends RequestSingle
return new RequestQuotaDto(self::getConnectorClass(), 1);
}
public function getAnalyticEvent(): ?Metrika\Events
{
return Metrika\Events::dataGeneration;
}
protected function getPrompt(): Prompt
{
$prompt = new Prompt('landing_ai_data');

View File

@@ -10,4 +10,5 @@ enum Errors: string
case requestError = 'REQUEST_ERROR';
case requestInvalid = 'REQUEST_INVALID';
case requestNotAllowed = 'REQUEST_NOT_ALLOWED';
case requestTimeout = 'REQUEST_TIMEOUT';
}

View File

@@ -5,6 +5,7 @@ namespace Bitrix\Landing\Mainpage;
use Bitrix\AI\Integration;
use Bitrix\Landing;
use Bitrix\Landing\Site\Type;
use Bitrix\Landing\Metrika;
use Bitrix\Main\Loader;
use Bitrix\Rest\AppTable;
use Bitrix\Rest\Configuration\Action\Import;
@@ -57,6 +58,12 @@ class Installer
return null;
}
$metrika = new Metrika\Metrika(
Metrika\Categories::Vibe,
Metrika\Events::createTemplateApi,
);
$metrika->setParam(1, 'templateCode', $code->value);
$app = AppTable::getByClientId($appCode);
$isAppInstalled =
!empty($app['ACTIVE'])
@@ -72,6 +79,8 @@ class Installer
|| isset($installResult['error'])
)
{
$metrika->setError($installResult['error'], Metrika\Statuses::ErrorMarket)->send();
return null;
}
}
@@ -83,6 +92,8 @@ class Installer
$zipId = (int)($appSite['ITEMS'][0]['ID'] ?? 0);
if ($zipId <= 0)
{
$metrika->setError('Wrong_zip_id', Metrika\Statuses::ErrorMarket)->send();
return null;
}
@@ -138,6 +149,8 @@ class Installer
}
}
$metrika->send();
return $newLandingId;
}
}

View File

@@ -8,6 +8,7 @@ use Bitrix\Main\Application;
enum TemplateRegions: string
{
//Templates::Enterprise
case EnterpriseWestAr = 'alaio.vibe_enterprise_west_ar';
case EnterpriseWestBr = 'alaio.vibe_enterprise_west_br';
case EnterpriseWestDe = 'alaio.vibe_enterprise_west_de';
@@ -27,58 +28,299 @@ enum TemplateRegions: string
case EnterpriseChineseEn = 'alaio.vibe_enterprise_chinese_en';
case EnterpriseChineseSc = 'alaio.vibe_enterprise_chinese_sc';
case EnterpriseChineseTc = 'alaio.vibe_enterprise_chinese_tc';
//for zones 'ru', 'by', 'kz'
//for zones 'ru', 'kz'
case EnterpriseRu = 'bitrix.vibe_enterprise_ru';
//for zone 'by'
case EnterpriseBy = 'bitrix.vibe_enterprise_by';
//Templates::Automation
case AutomationRu = 'bitrix.vibe_automation_ru';
case AutomationEn = 'alaio.vibe_automation_en';
case AutomationDe = 'alaio.vibe_automation_de';
case AutomationLa = 'alaio.vibe_automation_la';
case AutomationBr = 'alaio.vibe_automation_br';
case AutomationFr = 'alaio.vibe_automation_fr';
case AutomationPl = 'alaio.vibe_automation_pl';
case AutomationIt = 'alaio.vibe_automation_it';
case AutomationTr = 'alaio.vibe_automation_tr';
case AutomationJa = 'alaio.vibe_automation_ja';
case AutomationVn = 'alaio.vibe_automation_vn';
case AutomationAr = 'alaio.vibe_automation_ar';
case AutomationId = 'alaio.vibe_automation_id';
case AutomationKz = 'alaio.vibe_automation_kz';
case AutomationMs = 'alaio.vibe_automation_ms';
case AutomationTh = 'alaio.vibe_automation_th';
case AutomationChineseSc = 'alaio.vibe_automation_chinese_sc';
case AutomationChineseTc = 'alaio.vibe_automation_chinese_tc';
case AutomationChineseEn = 'alaio.vibe_automation_chinese_en';
//Templates::Collaboration
case CollaborationRu = 'bitrix.vibe_collaboration_ru';
case CollaborationEn = 'alaio.vibe_collaboration_en';
case CollaborationDe = 'alaio.vibe_collaboration_de';
case CollaborationLa = 'alaio.vibe_collaboration_la';
case CollaborationBr = 'alaio.vibe_collaboration_br';
case CollaborationFr = 'alaio.vibe_collaboration_fr';
case CollaborationPl = 'alaio.vibe_collaboration_pl';
case CollaborationIt = 'alaio.vibe_collaboration_it';
case CollaborationTr = 'alaio.vibe_collaboration_tr';
case CollaborationJa = 'alaio.vibe_collaboration_ja';
case CollaborationVn = 'alaio.vibe_collaboration_vn';
case CollaborationAr = 'alaio.vibe_collaboration_ar';
case CollaborationId = 'alaio.vibe_collaboration_id';
case CollaborationKz = 'alaio.vibe_collaboration_kz';
case CollaborationMs = 'alaio.vibe_collaboration_ms';
case CollaborationTh = 'alaio.vibe_collaboration_th';
case CollaborationChineseSc = 'alaio.vibe_collaboration_chinese_sc';
case CollaborationChineseTc = 'alaio.vibe_collaboration_chinese_tc';
case CollaborationChineseEn = 'alaio.vibe_collaboration_chinese_en';
//Templates::Boards
case BoardsRu = 'bitrix.vibe_boards_ru';
case BoardsEn = 'alaio.vibe_boards_en';
case BoardsDe = 'alaio.vibe_boards_de';
case BoardsLa = 'alaio.vibe_boards_la';
case BoardsBr = 'alaio.vibe_boards_br';
case BoardsFr = 'alaio.vibe_boards_fr';
case BoardsPl = 'alaio.vibe_boards_pl';
case BoardsIt = 'alaio.vibe_boards_it';
case BoardsTr = 'alaio.vibe_boards_tr';
case BoardsJa = 'alaio.vibe_boards_ja';
case BoardsVn = 'alaio.vibe_boards_vn';
case BoardsAr = 'alaio.vibe_boards_ar';
case BoardsId = 'alaio.vibe_boards_id';
case BoardsKz = 'alaio.vibe_boards_kz';
case BoardsMs = 'alaio.vibe_boards_ms';
case BoardsTh = 'alaio.vibe_boards_th';
case BoardsChineseSc = 'alaio.vibe_boards_chinese_sc';
case BoardsChineseTc = 'alaio.vibe_boards_chinese_tc';
case BoardsChineseEn = 'alaio.vibe_boards_chinese_en';
//Templates::Booking
case BookingRu = 'bitrix.vibe_booking_ru';
case BookingEn = 'alaio.vibe_booking_en';
case BookingDe = 'alaio.vibe_booking_de';
case BookingLa = 'alaio.vibe_booking_la';
case BookingBr = 'alaio.vibe_booking_br';
case BookingFr = 'alaio.vibe_booking_fr';
case BookingPl = 'alaio.vibe_booking_pl';
case BookingIt = 'alaio.vibe_booking_it';
case BookingTr = 'alaio.vibe_booking_tr';
case BookingJa = 'alaio.vibe_booking_ja';
case BookingVn = 'alaio.vibe_booking_vn';
case BookingAr = 'alaio.vibe_booking_ar';
case BookingId = 'alaio.vibe_booking_id';
case BookingKz = 'alaio.vibe_booking_kz';
case BookingMs = 'alaio.vibe_booking_ms';
case BookingTh = 'alaio.vibe_booking_th';
case BookingChineseSc = 'alaio.vibe_booking_chinese_sc';
case BookingChineseTc = 'alaio.vibe_booking_chinese_tc';
case BookingChineseEn = 'alaio.vibe_booking_chinese_en';
private const CIS_ZONES = ['ru', 'kz', 'by', 'uz'];
private const DEFAULT_CIS_ZONE = 'ru';
private const CHINESE_ZONES = ['cn', 'tc', 'sc'];
private const CONFIG_SECTION_CIS = 'cis';
private const CONFIG_SECTION_CHINESE = 'chinese';
private const CONFIG_SECTION_WEST = 'west';
private const DEFAULT_LANG = 'en';
/**
* Attempt to resolve template based on the provided code.
*
* @param Templates $code
*
* @return string|null
*/
public static function resolve(Templates $code): ?string
{
$regionCode = null;
return self::getTemplateCode($code->value)?->value;
}
switch ($code)
/**
* Get template code for specified template section.
*
* @param string $code
*
* @return self|null
*/
private static function getTemplateCode(string $code): ?self
{
$templateConfig = self::getTemplateConfig($code);
if (!$templateConfig)
{
case Templates::Enterprise:
$portalZone = \CBitrix24::getPortalZone();
$lang = Application::getInstance()->getContext()->getLanguage();
if (in_array($portalZone, ['ru', 'by', 'kz', 'uz']))
{
$regionCode = self::EnterpriseRu;
}
elseif (in_array($portalZone, ['cn', 'tc', 'sc']))
{
$regionCodes = [
'en' => self::EnterpriseChineseEn,
'sc' => self::EnterpriseChineseSc,
'tc' => self::EnterpriseChineseTc,
];
$regionCode = $regionCodes[$lang] ?? self::EnterpriseChineseEn;
}
else
{
$regionCodes = [
'ar' => self::EnterpriseWestAr,
'br' => self::EnterpriseWestBr,
'de' => self::EnterpriseWestDe,
'en' => self::EnterpriseWestEn,
'fr' => self::EnterpriseWestFr,
'id' => self::EnterpriseWestId,
'it' => self::EnterpriseWestIt,
'ja' => self::EnterpriseWestJa,
'kz' => self::EnterpriseWestKz,
'la' => self::EnterpriseWestLa,
'ms' => self::EnterpriseWestMs,
'pl' => self::EnterpriseWestPl,
'th' => self::EnterpriseWestTh,
'tr' => self::EnterpriseWestTr,
'vn' => self::EnterpriseWestVn,
'ru' => self::EnterpriseWestEn,
'ua' => self::EnterpriseWestEn,
];
$regionCode = $regionCodes[$lang] ?? self::EnterpriseWestEn;
}
break;
return null;
}
return $regionCode?->value;
$portalZone = \CBitrix24::getPortalZone();
//is CIS zone
if (in_array($portalZone, self::CIS_ZONES, true))
{
$cis = $templateConfig[self::CONFIG_SECTION_CIS];
return $cis[$portalZone] ?? $cis[self::DEFAULT_CIS_ZONE];
}
$lang = Application::getInstance()->getContext()->getLanguage();
//is Chinese zone
if (in_array($portalZone, self::CHINESE_ZONES, true))
{
return $templateConfig[self::CONFIG_SECTION_CHINESE][$lang]
?? $templateConfig[self::CONFIG_SECTION_CHINESE][self::DEFAULT_LANG];
}
return $templateConfig[self::CONFIG_SECTION_WEST][$lang]
?? $templateConfig[self::CONFIG_SECTION_WEST][self::DEFAULT_LANG];
}
/**
* Retrieve the configuration for the specified template code.
*
* @param string $code
*
* @return array|null
*/
private static function getTemplateConfig(string $code): ?array
{
$templatesConfig = [
Templates::Enterprise->value => [
self::CONFIG_SECTION_CIS => [
'by' => self::EnterpriseBy,
self::DEFAULT_CIS_ZONE => self::EnterpriseRu,
],
self::CONFIG_SECTION_CHINESE => [
'sc' => self::EnterpriseChineseSc,
'tc' => self::EnterpriseChineseTc,
self::DEFAULT_LANG => self::EnterpriseChineseEn,
],
self::CONFIG_SECTION_WEST => [
'ar' => self::EnterpriseWestAr,
'br' => self::EnterpriseWestBr,
'de' => self::EnterpriseWestDe,
'fr' => self::EnterpriseWestFr,
'id' => self::EnterpriseWestId,
'it' => self::EnterpriseWestIt,
'ja' => self::EnterpriseWestJa,
'kz' => self::EnterpriseWestKz,
'la' => self::EnterpriseWestLa,
'ms' => self::EnterpriseWestMs,
'pl' => self::EnterpriseWestPl,
'th' => self::EnterpriseWestTh,
'tr' => self::EnterpriseWestTr,
'vn' => self::EnterpriseWestVn,
'ru' => self::EnterpriseWestEn,
'ua' => self::EnterpriseWestEn,
self::DEFAULT_LANG => self::EnterpriseWestEn,
],
],
Templates::Automation->value => [
self::CONFIG_SECTION_CIS => [
self::DEFAULT_CIS_ZONE => self::AutomationRu,
],
self::CONFIG_SECTION_CHINESE => [
'sc' => self::AutomationChineseSc,
'tc' => self::AutomationChineseTc,
self::DEFAULT_LANG => self::AutomationChineseEn,
],
self::CONFIG_SECTION_WEST => [
'de' => self::AutomationDe,
'la' => self::AutomationLa,
'br' => self::AutomationBr,
'fr' => self::AutomationFr,
'pl' => self::AutomationPl,
'it' => self::AutomationIt,
'tr' => self::AutomationTr,
'ja' => self::AutomationJa,
'vn' => self::AutomationVn,
'ar' => self::AutomationAr,
'id' => self::AutomationId,
'kz' => self::AutomationKz,
'ms' => self::AutomationMs,
'th' => self::AutomationTh,
self::DEFAULT_LANG => self::AutomationEn,
],
],
Templates::Collaboration->value => [
self::CONFIG_SECTION_CIS => [
self::DEFAULT_CIS_ZONE => self::CollaborationRu,
],
self::CONFIG_SECTION_CHINESE => [
'sc' => self::CollaborationChineseSc,
'tc' => self::CollaborationChineseTc,
self::DEFAULT_LANG => self::CollaborationChineseEn,
],
self::CONFIG_SECTION_WEST => [
'de' => self::CollaborationDe,
'la' => self::CollaborationLa,
'br' => self::CollaborationBr,
'fr' => self::CollaborationFr,
'pl' => self::CollaborationPl,
'it' => self::CollaborationIt,
'tr' => self::CollaborationTr,
'ja' => self::CollaborationJa,
'vn' => self::CollaborationVn,
'ar' => self::CollaborationAr,
'id' => self::CollaborationId,
'kz' => self::CollaborationKz,
'ms' => self::CollaborationMs,
'th' => self::CollaborationTh,
self::DEFAULT_LANG => self::CollaborationEn,
],
],
Templates::Boards->value => [
self::CONFIG_SECTION_CIS => [
self::DEFAULT_CIS_ZONE => self::BoardsRu,
],
self::CONFIG_SECTION_CHINESE => [
'sc' => self::BoardsChineseSc,
'tc' => self::BoardsChineseTc,
self::DEFAULT_LANG => self::BoardsChineseEn,
],
self::CONFIG_SECTION_WEST => [
'de' => self::BoardsDe,
'la' => self::BoardsLa,
'br' => self::BoardsBr,
'fr' => self::BoardsFr,
'pl' => self::BoardsPl,
'it' => self::BoardsIt,
'tr' => self::BoardsTr,
'ja' => self::BoardsJa,
'vn' => self::BoardsVn,
'ar' => self::BoardsAr,
'id' => self::BoardsId,
'kz' => self::BoardsKz,
'ms' => self::BoardsMs,
'th' => self::BoardsTh,
self::DEFAULT_LANG => self::BoardsEn,
],
],
Templates::Booking->value => [
self::CONFIG_SECTION_CIS => [
self::DEFAULT_CIS_ZONE => self::BookingRu,
],
self::CONFIG_SECTION_CHINESE => [
'sc' => self::BookingChineseSc,
'tc' => self::BookingChineseTc,
self::DEFAULT_LANG => self::BookingChineseEn,
],
self::CONFIG_SECTION_WEST => [
'de' => self::BookingDe,
'la' => self::BookingLa,
'br' => self::BookingBr,
'fr' => self::BookingFr,
'pl' => self::BookingPl,
'it' => self::BookingIt,
'tr' => self::BookingTr,
'ja' => self::BookingJa,
'vn' => self::BookingVn,
'ar' => self::BookingAr,
'id' => self::BookingId,
'kz' => self::BookingKz,
'ms' => self::BookingMs,
'th' => self::BookingTh,
self::DEFAULT_LANG => self::BookingEn,
],
],
];
return $templatesConfig[$code] ?? null;
}
}

View File

@@ -7,4 +7,8 @@ namespace Bitrix\Landing\Mainpage;
enum Templates: string
{
case Enterprise = 'vibe_enterprise';
case Automation = 'vibe_automation';
case Collaboration = 'vibe_collaboration';
case Boards = 'vibe_boards';
case Booking = 'vibe_booking';
}

View File

@@ -14,6 +14,7 @@ enum Categories: string
case CrmForms = 'crm_forms';
case ExternalPictureEditor = 'external_picture_editor';
case SiteGeneration = 'site_generation';
case BlockEdition = 'block_edition';
public static function getBySiteType(string $siteType): Categories
{

View File

@@ -3,17 +3,22 @@ declare(strict_types=1);
namespace Bitrix\Landing\Metrika;
/**
* Enum representing all possible Metrika events.
*/
enum Events: string
{
case open = 'open';
case save = 'save';
case cancel = 'cancel';
case start = 'start';
case select = 'select';
case openStartPage = 'open_start_page';
case openSettingsMain = 'open_settings_main';
case openMarket = 'open_market';
case previewTemplate = 'preview_template';
case createTemplate = 'create_template';
case createTemplateApi = 'create_template_api';
case replaceTemplate = 'replace_template';
case openEditor = 'open_editor';
case publishSite = 'publish_site';
@@ -21,4 +26,10 @@ enum Events: string
case addWidget = 'add_widget';
case deleteWidget = 'delete_widget';
case clickOnButton = 'click_on_button';
case dataGeneration = 'data_generation';
case textsGeneration = 'texts_generation';
case imagesGeneration = 'images_generation';
case addFavourite = 'add_favourite';
case deleteFavourite = 'delete_favourite';
case unknown = 'unknown';
}

View File

@@ -11,8 +11,77 @@ class FieldsDto
public ?Sections $section = null,
public ?string $subSection = null,
public ?string $element = null,
/**
* @var array|null - array of arrays [position, name, value]
* F.e.
* [
* [1, foo, bar],
* [2, baz, bat],
* ]
*/
public ?array $params = null,
public ?string $error = null,
)
{}
public function toArray(): array
{
return [
'event' => $this->event,
'type' => $this->type,
'section' => $this->section,
'subSection' => $this->subSection,
'element' => $this->element,
'params' => $this->params,
'error' => $this->error,
];
}
public static function fromArray(array $data): self
{
if (isset($data['event']))
{
$event = Events::tryFrom($data['event']);
}
if (isset($data['type']))
{
$type = Types::tryFrom($data['type']);
}
if (isset($data['section']))
{
$section = Sections::tryFrom($data['section']);
}
$subSection = $data['subSection'] ?? null;
$element = $data['element'] ?? null;
$params = $data['params'] ?? null;
$error = $data['error'] ?? null;
return new self(
$event,
$type,
$section,
$subSection,
$element,
$params,
$error,
);
}
public function addFields(?FieldsDto $addFields): self
{
if ($addFields === null)
{
return $this;
}
$this->event = $addFields->event ?? $this->event;
$this->type = $addFields->type ?? $this->type;
$this->section = $addFields->section ?? $this->section;
$this->subSection = $addFields->subSection ?? $this->subSection;
$this->element = $addFields->element ?? $this->element;
$this->params = $addFields->params ?? $this->params;
$this->error = $addFields->error ?? $this->error;
return $this;
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Bitrix\Landing\Metrika;
use Bitrix\AI\Tuning;
use Bitrix\Landing\Connector\Ai;
use Bitrix\Main\Loader;
/**
* Service for managing and setting provider parameters in Metrika analytics events.
*
* This service encapsulates the logic for mapping Metrika events to AI provider codes,
* retrieving provider codes from the Tuning manager, and setting provider-related parameters
* in Metrika analytics objects.
*/
class MetrikaProviderParamService
{
/**
* Position of the provider parameter in the Metrika params array.
* @var int
*/
private const PROVIDER_PARAM_POSITION = 2;
/**
* Name of the provider parameter in Metrika.
* @var string
*/
private const PROVIDER_PARAM_NAME = 'provider';
/**
* Tuning manager instance for retrieving provider codes.
* @var Tuning\Manager
*/
private Tuning\Manager $tuningManager;
/**
* MetrikaProviderParamService constructor.
*
* @param Tuning\Manager|null $tuningManager Optional tuning manager instance. If not provided, a new instance will
* be created.
*/
public function __construct(?Tuning\Manager $tuningManager = null)
{
if (Loader::includeModule('ai'))
{
$this->tuningManager = $tuningManager ?? new Tuning\Manager();
}
}
/**
* Sets all relevant parameters in the Metrika analytics object for the specified event.
* Implements MetrikaParamSetterInterface.
*
* @param Metrika $metrika Metrika analytics object.
* @param Events $event Metrika event.
*
* @return void
*/
public function setParams(Metrika $metrika, Events $event): void
{
$this->setProviderParam($metrika, $event);
}
/**
* Sets the provider parameter in the given Metrika analytics object for the specified event.
*
* @param Metrika $metrika Metrika analytics object.
* @param Events $event Metrika event.
*
* @return void
*/
private function setProviderParam(Metrika $metrika, Events $event): void
{
$providerCode = $this->getProviderCodeByEvent($event);
if ($providerCode !== null)
{
$metrika->setParam(
self::PROVIDER_PARAM_POSITION,
self::PROVIDER_PARAM_NAME,
$providerCode
);
}
}
/**
* Returns the provider code for a given Metrika event.
*
* @param Events $event Metrika event.
*
* @return string|null Provider code if found, null otherwise.
*/
private function getProviderCodeByEvent(Events $event): ?string
{
$providerSetting = self::getProviderSettingName($event->value);
if (!isset($providerSetting))
{
return null;
}
$item = $this->tuningManager->getItem($providerSetting);
if ($item === null)
{
return null;
}
$providerCode = $item->getValue();
if (empty($providerCode))
{
return null;
}
return $item->getOptions()[$providerCode] ?? null;
}
/**
* Get setting name for AI tuning
* @param string $eventName
* @return string|null
*/
public static function getProviderSettingName(string $eventName): ?string
{
$event = Events::tryFrom($eventName);
return match ($event)
{
Events::dataGeneration,
Events::textsGeneration => Ai::TUNING_CODE_SITE_TEXT_PROVIDER,
Events::imagesGeneration => Ai::TUNING_CODE_SITE_IMAGE_PROVIDER,
default => null,
};
}
}

View File

@@ -7,7 +7,13 @@ enum Statuses: string
{
case Success = 'success';
case Error = 'error';
case ErrorContentPolicy = 'error_content_policy';
case ErrorB24 = 'error_b24';
case ErrorProvider = 'error_provider';
case ErrorLimitDaily = 'error_limit_daily';
case ErrorLimitMonthly = 'error_limit_monthly';
case ErrorLimitBaas = 'error_limit_baas';
case ErrorMarket = 'error_market';
case ErrorTurnedOff = 'error_turnedoff';
case UnsupportedBlock = 'unsupported_block';
}

View File

@@ -2,6 +2,7 @@
namespace Bitrix\Landing\Assets;
use Bitrix\Landing\Site\Type;
use \Bitrix\Main\Localization\Loc;
use Bitrix\Main\UI\Extension;
use Bitrix\Main;
@@ -151,14 +152,13 @@ class Manager
self::REGISTERED_KEY_CODE => $code,
self::REGISTERED_KEY_LOCATION => $location,
];
if($code !== 'main.core' && $code !== 'core')
if ($code !== 'main.core' && $code !== 'core')
{
\CJSCore::markExtensionLoaded($code);
}
}
/**
* Recursive (by 'rel' key) adding assets in WP packege
*
@@ -200,14 +200,22 @@ class Manager
// get data from CJSCore
if ($ext = \CJSCore::getExtInfo($code))
{
if (!Type::isExtensionAllow($code))
{
return;
}
$asset = $ext;
}
else if ($ext = Extension::getConfig($code))
elseif ($ext = Extension::getConfig($code))
{
if (!Type::isExtensionAllow($code))
{
return;
}
$asset = $ext;
}
// if name - it path
else if ($type = self::detectType($code))
elseif ($type = self::detectType($code))
{
$asset = [$type => [$code]];
}
@@ -243,7 +251,6 @@ class Manager
return true;
}
/**
* Get parts of asset and add them in pack
*
@@ -286,7 +293,7 @@ class Manager
$this->resources->addString($this->createStringFromPath($path, $type));
}
// todo: check is file exist
else if (self::detectType($path))
elseif (self::detectType($path))
{
$this->resources->add($path, $type, $location);
}
@@ -355,7 +362,6 @@ class Manager
return $externalLink;
}
/**
* Detect type by path.
*

View File

@@ -4447,8 +4447,12 @@ class Block extends \Bitrix\Landing\Internals\BaseTable
if ($resultNode)
{
$contentBefore = $resultNode->getOuterHTML();
if ((int)$resultNode->getNodeType() === $resultNode::ELEMENT_NODE)
$contentBefore = $resultNode->getOuterHTML();
if (
isset($data[$relativeSelector]['classList'])
&& is_array($data[$relativeSelector]['classList'])
&& (int)$resultNode->getNodeType() === $resultNode::ELEMENT_NODE
)
{
$resultNode->setClassName(
implode(' ', $data[$relativeSelector]['classList'])

View File

@@ -11,7 +11,6 @@ use Bitrix\Landing\Manager;
use Bitrix\Landing\Repo;
use Bitrix\Landing\Internals;
use Bitrix\Landing\Site\Type;
use Bitrix\Main\Application;
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
use Bitrix\Main\Loader;
@@ -46,6 +45,7 @@ class BlockRepo
* Sections with special conditions
*/
private const SECTION_LAST = 'last';
private const SECTION_FAVOURITE = 'favourite';
/**
* Section or block type with special conditions
@@ -264,7 +264,7 @@ class BlockRepo
if ($cache->initCache($cacheTime, $cacheId, $cachePath))
{
$this->repository = $cache->getVars();
if (is_array($this->repository) && !empty($this->repository))
if (!empty($this->repository))
{
return $this->fillLastUsedBlocks();
}
@@ -582,8 +582,7 @@ class BlockRepo
private function fillLastUsedBlocks(): static
{
$request = Application::getInstance()->getContext()->getRequest();
if ($request->get('landing_mode') !== 'edit')
if (Landing::getEditMode() === false)
{
return $this;
}
@@ -609,9 +608,9 @@ class BlockRepo
foreach ($cat['items'] as $code => &$block)
{
if (
in_array($code, $lastUsed)
&& $catCode != self::SECTION_LAST
$catCode !== self::SECTION_LAST
&& !empty($block)
&& in_array($code, $lastUsed, true)
)
{
$block['section'][] = self::SECTION_LAST;
@@ -676,17 +675,17 @@ class BlockRepo
* @param string|array $item
* @return array|null
*/
$prepareType = function (string|array $item): ?array
$prepareType = static function (string|array $item): ?array
{
$type = (array)$item;
$type = array_map('strtoupper', $type);
if (in_array('PAGE', $type))
if (in_array('PAGE', $type, true))
{
$type[] = 'SMN';
}
if (
in_array('NULL', $type)
|| in_array('', $type)
in_array('NULL', $type, true)
|| in_array('', $type, true)
)
{
return null;
@@ -706,17 +705,17 @@ class BlockRepo
$sectionTypes = $prepareType($section['type'] ?? []);
if (
$this->isFilterActive(self::FILTER_SKIP_COMMON_BLOCKS)
&& empty($sectionTypes)
&& $sectionCode !== self::SECTION_LAST
empty($sectionTypes)
&& $this->isFilterActive(self::FILTER_SKIP_COMMON_BLOCKS)
&& !in_array($sectionCode, [self::SECTION_LAST, self::SECTION_FAVOURITE], true)
)
{
continue;
}
if (
$this->isFilterActive(self::FILTER_SKIP_HIDDEN_BLOCKS)
&& $sectionTypes === null
$sectionTypes === null
&& $this->isFilterActive(self::FILTER_SKIP_HIDDEN_BLOCKS)
)
{
continue;
@@ -743,16 +742,16 @@ class BlockRepo
$blockTypes = $prepareType($block['type'] ?? []);
if (
$this->isFilterActive(self::FILTER_SKIP_COMMON_BLOCKS)
&& empty($blockTypes)
empty($blockTypes)
&& $this->isFilterActive(self::FILTER_SKIP_COMMON_BLOCKS)
)
{
continue;
}
if (
$this->isFilterActive(self::FILTER_SKIP_HIDDEN_BLOCKS)
&& $blockTypes === null
$blockTypes === null
&& $this->isFilterActive(self::FILTER_SKIP_HIDDEN_BLOCKS)
)
{
continue;
@@ -773,9 +772,9 @@ class BlockRepo
}
if (
$this->isFilterActive(self::FILTER_SKIP_SYSTEM_BLOCKS)
&& isset($block['system'])
isset($block['system'])
&& $block['system'] === true
&& $this->isFilterActive(self::FILTER_SKIP_SYSTEM_BLOCKS)
)
{
continue;
@@ -788,13 +787,40 @@ class BlockRepo
$filtered[$sectionCode]['items'][$blockCode] = $block;
}
if (empty($filtered[$sectionCode]['items']))
if (empty($filtered[$sectionCode]['items']) && $sectionCode !== self::SECTION_FAVOURITE)
{
unset($filtered[$sectionCode]);
}
}
return $filtered;
return $this->filterLastUsed($filtered);
}
private function filterLastUsed(array $repository): array
{
if (!isset($repository[self::SECTION_LAST]['items']))
{
return $repository;
}
$removeLastSection = static function(array $sections)
{
return array_diff($sections, [self::SECTION_LAST]);
};
$filteredBlocks = [];
$allowableSections = $removeLastSection(array_keys($repository));
foreach ($repository[self::SECTION_LAST]['items'] as $code => $block)
{
$blockSections = $removeLastSection($block['section'] ?? []);
if (!empty(array_intersect($blockSections, $allowableSections)))
{
$filteredBlocks[$code] = $block;
}
}
$repository[self::SECTION_LAST]['items'] = $filteredBlocks;
return $repository;
}
/**

View File

@@ -2,6 +2,7 @@
namespace Bitrix\Landing\Connector;
use \Bitrix\Landing\Manager;
use \Bitrix\Main\Loader;
use \Bitrix\Main\Localization\Loc;
use \Bitrix\Main\Web\Json;
use \Bitrix\MobileApp\Janative;
@@ -24,6 +25,11 @@ class Mobile
*/
public static function onMobileMenuStructureBuilt($menu): array
{
if (!Loader::includeModule('mobileapp'))
{
return $menu;
}
if (!isset($menu[0]['items']) || !is_array($menu[0]['items']))
{
return $menu;
@@ -163,7 +169,7 @@ JS
if ($mobileHit === null)
{
$mobileHit = \Bitrix\Main\ModuleManager::isModuleInstalled('intranet')
&& mb_strpos(Manager::getCurDir(), SITE_DIR . 'mobile/') === 0;
&& mb_strpos(Manager::getCurDir(), SITE_DIR . 'mobile/') === 0;
}
return $mobileHit;

View File

@@ -8,6 +8,7 @@ use Bitrix\Landing;
use Bitrix\Landing\Copilot\Generation;
use Bitrix\Landing\Copilot\Data;
use Bitrix\Landing\Copilot\Connector;
use Bitrix\Landing\Metrika;
use Bitrix\Main\Error;
class Copilot extends Controller
@@ -54,10 +55,26 @@ class Copilot extends Controller
public static function checkBlockGeneratableAction(int $blockId, ?int $chatId = null): bool
{
$result = Data\Block\Operator::isBlockAvailableForScenarioChangeBlock($blockId);
$isGeneratable = Data\Block\Operator::isBlockAvailableForScenarioChangeBlock($blockId);
$metrika = new Metrika\Metrika(
Metrika\Categories::BlockEdition,
Metrika\Events::select,
Metrika\Tools::ai
);
$metrika
->setSection(Metrika\Sections::siteEditor)
->setParam(4, 'block', (new Landing\Block($blockId))->getCode())
;
if (!$isGeneratable)
{
$metrika->setStatus(Metrika\Statuses::UnsupportedBlock);
}
$metrika->send();
if (!$chatId || $chatId <= 0)
{
return $result;
return $isGeneratable;
}
/**
@@ -67,12 +84,12 @@ class Copilot extends Controller
if ($chatBot)
{
$message = new Landing\Copilot\Connector\Chat\ChatBotMessageDto($chatId);
$result
$isGeneratable
? $chatBot->sendSelectBlockSuccessMessage($message)
: $chatBot->sendSelectBlockWrongMessage($message);
}
return $result;
return $isGeneratable;
}
public function sendBlockGenerationNeedSelectMessageAction(int $siteId)
@@ -131,6 +148,15 @@ class Copilot extends Controller
->setSiteData($siteData)
->setWishes((new Data\Wishes())->setWishes([$wishes]))
->setChatId($chat->getChatForSite($siteId) ?? 0)
->setMetrikaFields((new Metrika\FieldsDto(
params: [
[
4,
'block',
(new Landing\Block($blockId))->getCode(),
],
]
)))
;
if ($generation->execute())

View File

@@ -12,7 +12,17 @@ class Landing extends Controller
{
return [
new Engine\ActionFilter\Authentication(),
new ActionFilter\Extranet(),
];
}
public function configureActions(): array
{
return [
'getByIdAction' => [
'+prefilters' => [
new ActionFilter\Extranet(),
],
]
];
}
@@ -40,4 +50,9 @@ class Landing extends Controller
return null;
}
public function isPhoneRegionCodeTourAlreadySeenAction(): bool
{
return \CUserOptions::GetOption('ui-tour', 'landing_phone_aha_shown', null) !== null;
}
}

View File

@@ -4,14 +4,20 @@ namespace Bitrix\Landing\Controller;
use Bitrix\Main\Engine;
use Bitrix\Main\Engine\Controller;
use Bitrix\Landing;
use Bitrix\Landing\Copilot\Generation;
use Bitrix\Landing\Copilot\Data;
use Bitrix\Landing\Copilot\Connector;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Web\HttpClient;
use Bitrix\Landing;
use Bitrix\Landing\Manager;
use Bitrix\Landing\Assets;
use Bitrix\Landing\Copilot\Connector;
use Bitrix\Rest;
class Vibe extends Controller
{
private const SUBTYPE_WIDGET = 'widgetvue';
public function getDefaultPreFilters(): array
{
return [
@@ -21,13 +27,164 @@ class Vibe extends Controller
}
/**
* Save relations between chat, site and user
* @param int $siteId
* @param int $chatId
* @return bool
* Get core extensions and styles configs, load relations, load lang phrases
*
* @return array - array of assets by type
*/
public function setDemoTestAction(): bool
public static function getCoreConfigAction(): array
{
return (bool)\CBitrix24::setLicenseType('demo');
$coreExts = [
'main.core',
'ui.design-tokens',
];
$assetsManager = (new Assets\Manager())
->enableSandbox()
->addAsset($coreExts)
;
$siteTemplatePath =
(defined('SITE_TEMPLATE_PATH') ? SITE_TEMPLATE_PATH : '/bitrix/templates/bitrix24');
$style = $siteTemplatePath . '/dist/bitrix24.bundle.css';
$assetsManager->addAsset($style);
return $assetsManager->getOutput();
}
/**
* Get extensions configs, load relations, load lang phrases
*
* @param array $extCodes - array of extensions codes
* @return array - array of assets by type
*/
public static function getAssetsConfigAction(array $extCodes): array
{
$assetsManager = (new Assets\Manager())
->enableSandbox()
->addAsset($extCodes)
;
return $assetsManager->getOutput();
}
/**
* @param int $blockId
* @param array $params
*/
public function fetchDataAction(int $blockId, array $params = [])
{
$block = new Landing\Block($blockId);
if (!$block->getId())
{
$this->addError(
new Error(Loc::getMessage('LANDING_WIDGET_BLOCK_NOT_FOUND'), 'BLOCK_NOT_FOUND')
);
return null;
}
if (!Loader::includeModule('rest'))
{
$this->addError(
new Error(Loc::getMessage('LANDING_WIDGET_REST_NOT_FOUND'), 'REST_NOT_FOUND')
);
return null;
}
// check app
$repoId = $block->getRepoId();
$app = Landing\Repo::getAppInfo($repoId);
if (
!$repoId
|| empty($app)
|| !isset($app['CLIENT_ID'])
)
{
$this->addError(
new Error(Loc::getMessage('LANDING_WIDGET_APP_NOT_FOUND'), 'APP_NOT_FOUND')
);
return null;
}
// get auth
$appHasAccess = \CRestUtil::checkAppAccess($app['ID'] ?? 0);
if (!$appHasAccess)
{
$this->addError(
// todo: open after translate
// new Error(Loc::getMessage('LANDING_WIDGET_APP_NO_ACCESS'), 'APP_NO_ACCESS')
new Error('Landing widget app has no access', 'APP_NO_ACCESS')
);
return null;
}
$auth = Rest\Application::getAuthProvider()?->get(
$app['CLIENT_ID'],
'landing',
[],
Manager::getUserId()
);
if ($auth && isset($auth['error']))
{
$this->addError(
new Error(
$auth['error_description'] ?? '',
'APP_AUTH_ERROR__' . $auth['error']
)
);
return null;
}
$params['auth'] = $auth;
// check subtype
$manifest = $block->getManifest();
if (
!in_array(self::SUBTYPE_WIDGET, (array)$manifest['block']['subtype'], true)
|| !is_array($manifest['block']['subtype_params'])
|| !isset($manifest['block']['subtype_params']['handler'])
)
{
$this->addError(
new Error(Loc::getMessage('LANDING_WIDGET_HANDLER_NOT_FOUND_2'), 'HANDLER_NOT_FOUND')
);
return null;
}
// request
$url = (string)$manifest['block']['subtype_params']['handler'];
$http = new HttpClient();
$data = $http->post(
$url,
$params
);
if ($http->getStatus() !== 200)
{
$this->addError(
new Error(Loc::getMessage('LANDING_WIDGET_HANDLER_NOT_ALLOW'), 'HANDLER_NOT_ALLOW')
);
return null;
}
$type = empty($params) ? 'fetch' : 'fetch_params';
Rest\UsageStatTable::logLandingWidget($app['CLIENT_ID'], $type);
Rest\UsageStatTable::finalize();
if (isset($data['error']))
{
$this->addError(
new Error($data['error'], $data['error_description'] ?? '')
);
return null;
}
return $data;
}
}

View File

@@ -31,6 +31,7 @@ class Text extends \Bitrix\Landing\Field
$this->searchable = isset($params['searchable']) && $params['searchable'] === true;
$this->placeholder = isset($params['placeholder']) ? $params['placeholder'] : '';
$this->maxlength = isset($params['maxlength']) ? (int)$params['maxlength'] : 0;
$this->fetchModificator = isset($params['fetch_data_modification']) ? $params['fetch_data_modification'] : null;
}
/**

View File

@@ -5,6 +5,7 @@ use Bitrix\Landing\Field;
use Bitrix\Landing\Manager;
use Bitrix\Main\Loader;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Page\Asset;
use Bitrix\UI;
class Fonts extends \Bitrix\Landing\Hook\Page
@@ -65,6 +66,11 @@ class Fonts extends \Bitrix\Landing\Hook\Page
],
];
/**
* Default domain for Google Fonts.
*/
public const DEFAULT_DOMAIN = 'fonts.googleapis.com';
/**
* Set fonts on the page.
* @var array
@@ -164,10 +170,7 @@ class Fonts extends \Bitrix\Landing\Hook\Page
if ($setFonts)
{
Manager::setPageView(
'BeforeHeadClose',
implode('', $setFonts)
);
Asset::getInstance()->addString(implode('', $setFonts));
}
}
@@ -211,7 +214,8 @@ class Fonts extends \Bitrix\Landing\Hook\Page
*/
public static function generateFontTags(string $fontName): string
{
$fontUrl = "https://fonts.bitrix24.ru/css2?family="
$proxyDomain = self::getProxyDomain();
$fontUrl = "https://{$proxyDomain}/css2?family="
. str_replace(' ', '+', $fontName)
. ":wght@100;200;300;400;500;600;700;800;900";
$fontClass = strtolower(str_replace(' ', '-', $fontName));
@@ -221,7 +225,7 @@ class Fonts extends \Bitrix\Landing\Hook\Page
<link rel="preload" href="$fontUrl" data-font="g-font-$fontClass" onload="this.removeAttribute('onload');this.rel='stylesheet'" as="style">
<style data-id="g-font-$fontClass">.g-font-$fontClass { font-family: "$fontName", sans-serif; }</style>
HTML;
}
}
/**
* Proxy font url to bitrix servers
@@ -230,16 +234,28 @@ class Fonts extends \Bitrix\Landing\Hook\Page
*/
protected static function proxyFontUrl(string $fontString): string
{
$defaultDomain = 'fonts.googleapis.com';
$proxyDomain = $defaultDomain;
if (Loader::includeModule('ui'))
{
$proxyDomain = UI\Fonts\Proxy::resolveDomain(Manager::getZone());
}
$defaultDomain = self::DEFAULT_DOMAIN;
$proxyDomain = self::getProxyDomain();
return ($defaultDomain !== $proxyDomain)
? str_replace($defaultDomain, $proxyDomain, $fontString)
: $fontString
;
}
/**
* Returns the proxy domain for fonts, depending on the current zone and UI module.
*
* @return string
*/
private static function getProxyDomain(): string
{
$proxyDomain = self::DEFAULT_DOMAIN;
if (Loader::includeModule('ui'))
{
$proxyDomain = UI\Fonts\Proxy::resolveDomain(Manager::getZone());
}
return $proxyDomain;
}
}

View File

@@ -76,7 +76,7 @@ class PixelVk extends \Bitrix\Landing\Hook\Page
var t=document.createElement("script");
t.type="text/javascript",
t.async=!0,
t.src="https://vk.com/js/api/openapi.js?160",
t.src="https://vk.ru/js/api/openapi.js?160",
t.onload=function(){VK.Retargeting.Init("' . $counter . '"),
VK.Retargeting.Hit()},document.head.appendChild(t)
}();'
@@ -84,7 +84,7 @@ class PixelVk extends \Bitrix\Landing\Hook\Page
Manager::setPageView(
'Noscript',
'<noscript>
<img src="https://vk.com/rtrg?p=' . $counter . '" style="position:fixed; left:-999px;" alt=""/>
<img src="https://vk.ru/rtrg?p=' . $counter . '" style="position:fixed; left:-999px;" alt=""/>
</noscript>'
);
}

View File

@@ -48,6 +48,7 @@ class Settings extends \Bitrix\Landing\Hook\Page
'BRAND_PROPERTY' => 'BRAND_REF',
'CART_POSITION' => 'BL',
'AGREEMENT_ID' => 0,
'AGREEMENTS' => null,
);
/**
@@ -156,9 +157,8 @@ class Settings extends \Bitrix\Landing\Hook\Page
default:
{
$field = new Field\Text($code, array(
'title' => isset($params['NAME'])
? $params['NAME']
: ''
'title' => $params['NAME'] ?? '',
'fetch_data_modification' => $params['FETCH_DATA_MODIFICATION'] ?? null,
));
break;
}
@@ -322,6 +322,40 @@ class Settings extends \Bitrix\Landing\Hook\Page
$fields['AGREEMENT_ID'] = self::getFieldByType(
null, 'AGREEMENT_ID'
);
$fields['AGREEMENTS'] = self::getFieldByType(
null,
'AGREEMENTS',
[
'FETCH_DATA_MODIFICATION' => function ($agreements) {
if (!is_array($agreements))
{
return null;
}
$resultAgreements = [];
$existAgreementIds = [];
foreach ($agreements as $agreement)
{
$agreement['ID'] = (int)$agreement['ID'];
if (
empty($agreement['ID'])
|| isset($existAgreementIds[$agreement['ID']])
)
{
continue;
}
$existAgreementIds[$agreement['ID']] = true;
$resultAgreements[] = [
'ID' => $agreement['ID'],
'CHECKED' => $agreement['CHECKED'] === 'Y' ? 'Y' : 'N',
'REQUIRED' => $agreement['REQUIRED'] === 'Y' ? 'Y' : 'N',
];
}
return $resultAgreements;
}
],
);
// cart position
$positions = array_fill_keys(
@@ -423,11 +457,19 @@ class Settings extends \Bitrix\Landing\Hook\Page
if($hooks['SETTINGS']['AGREEMENT_USE'] === 'N')
{
$settings[$id]['AGREEMENT_ID'] = 0;
$settings[$id]['AGREEMENTS'] = [];
}
}
else
{
$settings[$id]['AGREEMENT_USE'] = $settings[$id]['AGREEMENT_ID'] ? 'Y' : 'N';
if ($settings[$id]['AGREEMENTS'] === null)
{
$settings[$id]['AGREEMENT_USE'] = $settings[$id]['AGREEMENT_ID'] ? 'Y' : 'N';
}
else
{
$settings[$id]['AGREEMENT_USE'] = $settings[$id]['AGREEMENTS'] ? 'Y' : 'N';
}
}
if (isset($hooks['SETTINGS']['CART_POSITION']))

View File

@@ -0,0 +1,50 @@
<?php
namespace Bitrix\Landing\Internals;
use \Bitrix\Main\Entity;
use \Bitrix\Main\Localization\Loc;
Loc::loadMessages(__FILE__);
/**
* Class BlockFavouriteTable
*/
class BlockFavouriteTable extends Entity\DataManager
{
/**
* Returns DB table name for entity.
*
* @return string
*/
public static function getTableName(): string
{
return 'b_landing_block_favourite';
}
/**
* Returns entity map definition.
*
* @return array
*/
public static function getMap(): array
{
return array(
'ID' => new Entity\IntegerField('ID', array(
'title' => 'ID',
'primary' => true,
'autocomplete' => true,
)),
'USER_ID' => new Entity\IntegerField('USER_ID', array(
'title' => Loc::getMessage('LANDING_TABLE_BLOCK_FAVOURITE_FIELD_USER_ID'),
'required' => true
)),
'CODE' => new Entity\StringField('CODE', array(
'title' => Loc::getMessage('LANDING_TABLE_BLOCK_FAVOURITE_FIELD_CODE'),
'required' => true
)),
'DATE_CREATE' => new Entity\DatetimeField('DATE_CREATE', array(
'title' => Loc::getMessage('LANDING_TABLE_BLOCK_FAVOURITE_FIELD_DATE_CREATE')
))
);
}
}

View File

@@ -93,7 +93,7 @@ class SiteTable extends Entity\DataManager
'default_value' => 'Y'
)),
'DELETED' => new Entity\StringField('DELETED', array(
'title' => Loc::getMessage('LANDING_TABLE_FIELD_LANDING_DELETED'),
'title' => Loc::getMessage('LANDING_TABLE_FIELD_SITE_DELETED'),
'default_value' => 'N'
)),
'TITLE' => new Entity\StringField('TITLE', array(
@@ -937,34 +937,7 @@ class SiteTable extends Entity\DataManager
{
try
{
//todo: revert changes after change .by domain
if (
!str_ends_with($domainName, '.b24site.online')
&& !str_ends_with($domainName, '.b24shop.online')
)
{
$domainExist = $siteController::isDomainExists($domainName);
}
else
{
$byDomainName = '';
if (str_ends_with($domainName, '.b24site.online'))
{
$byDomainName = str_replace('.b24site.online', '.bitrix24site.by', $domainName);
}
if (str_ends_with($domainName, '.b24shop.online'))
{
$byDomainName = str_replace('.b24shop.online', '.bitrix24shop.by', $domainName);
}
if ($byDomainName !== '' && $siteController::isDomainExists($byDomainName))
{
$domainExist = true;
}
else
{
$domainExist = $siteController::isDomainExists($domainName);
}
}
$domainExist = $siteController::isDomainExists($domainName);
}
catch (SystemException $ex)
{

View File

@@ -1639,7 +1639,14 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
$urls['LANDING'][$lid] = \htmlspecialcharsbx($urls['LANDING'][$lid]);
}
$urls['LANDING'][$lid] .= ($isIframe ? '?IFRAME=Y' : '');
$urls['BLOCK'][$bid] = $urls['LANDING'][$lid] . '#' . $anchorsPublicId[$bid];
if (Site\Type::getCurrentScopeId() === Site\Type::SCOPE_CODE_MAINPAGE)
{
$urls['BLOCK'][$bid] = '#' . $anchorsPublicId[$bid];
}
else
{
$urls['BLOCK'][$bid] = $urls['LANDING'][$lid] . '#' . $anchorsPublicId[$bid];
}
}
else
{
@@ -2281,11 +2288,6 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
*/
public function addBlock(string $code, array $data = array(), bool $saveInLastUsed = false)
{
$metrika = new Metrika\Metrika(
Metrika\Categories::getBySiteType(self::$siteType),
Metrika\Events::addWidget,
);
if (!$this->canEdit())
{
$this->error->addError(
@@ -2293,12 +2295,9 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED')
);
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
if ($saveInLastUsed)
{
$metrika
->setError('ACCESS_DENIED')
->send()
;
$this->sendAddBlockErrorMetrika('ACCESS_DENIED');
}
return false;
@@ -2316,6 +2315,7 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
if ($saveInLastUsed)
{
Block::markAsUsed($code);
$this->sendAddBlockMetrika($block, $data);
}
$this->touch();
@@ -2327,25 +2327,69 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
$history->push('ADD_BLOCK', ['block' => $block]);
}
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
{
$this->parametrizeMetrikaByBlock($metrika, $block)->send();
}
return $block->getId();
}
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
$this->error->addError(
'BLOCK_NOT_FOUND',
Loc::getMessage('LANDING_BLOCK_NOT_FOUND')
);
if ($saveInLastUsed)
{
$metrika
->setError('BLOCK_NOT_FOUND')
->send()
;
$this->sendAddBlockErrorMetrika('BLOCK_NOT_FOUND');
}
return false;
}
/**
* Creates a Metrika object for the addWidget event.
*
* @param Metrika\Events $event The event name for which the Metrika object is created.
*
* @return Metrika\Metrika The created Metrika object.
*/
protected function createMetrika(Metrika\Events $event): Metrika\Metrika
{
return new Metrika\Metrika(
Metrika\Categories::getBySiteType(self::$siteType),
$event,
);
}
/**
* Send add block metrika if needed.
*
* @param array $data
* @param Block $block
*
* @return void
*/
private function sendAddBlockMetrika(Block $block, array $data): void
{
$metrika = $this->createMetrika(Metrika\Events::addWidget);
$metrika->setSubSection($data['CATEGORY'] ?? '');
$this->parametrizeMetrikaByBlock($metrika, $block)->send();
$metrika->send();
}
/**
* Sends a Metrika event for an error that occurred while adding a block.
*
* @param string $error The error code or message to be sent to Metrika.
*
* @return void
*/
private function sendAddBlockErrorMetrika(string $error): void
{
$metrika = $this->createMetrika(Metrika\Events::addWidget);
$metrika->setError($error)->send();
}
/**
* Delete one block from current landing.
* @param int $id Block id.
@@ -2393,10 +2437,7 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
$this->blocks[$id] = new Block($id);
}
$metrika = new Metrika\Metrika(
Metrika\Categories::getBySiteType(self::$siteType),
Metrika\Events::deleteWidget,
);
$metrika = $this->createMetrika(Metrika\Events::deleteWidget);
if (
isset($this->blocks[$id]) &&
@@ -2415,13 +2456,10 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
}
if ($this->blocks[$id]->save())
{
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
{
$this
->parametrizeMetrikaByBlock($metrika, $this->blocks[$id])
->send()
;
}
$this
->parametrizeMetrikaByBlock($metrika, $this->blocks[$id])
->send()
;
if ($mark)
{
@@ -2449,13 +2487,10 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
$this->blocks[$id]->getError()
);
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
{
$metrika
->setError('SAVE_ERROR')
->send()
;
}
$metrika
->setError('SAVE_ERROR')
->send()
;
}
}
else
@@ -2464,13 +2499,10 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
'ACCESS_DENIED',
Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED')
);
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
{
$metrika
->setError('ACCESS_DENIED')
->send()
;
}
$metrika
->setError('ACCESS_DENIED')
->send()
;
return false;
}
@@ -2481,13 +2513,10 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
'BLOCK_NOT_FOUND',
Loc::getMessage('LANDING_BLOCK_NOT_FOUND')
);
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
{
$metrika
->setError('BLOCK_NOT_FOUND')
->send()
;
}
$metrika
->setError('BLOCK_NOT_FOUND')
->send()
;
return false;
}
@@ -2497,9 +2526,6 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
protected function parametrizeMetrikaByBlock(Metrika\Metrika $metrika, Block $block): Metrika\Metrika
{
// todo: Now method callsed just for widget
// todo: If need for all types - add self::$siteType === Type::SCOPE_CODE_MAINPAGE checking
if ($block->getRepoId())
{
$metrika->setType(Types::widgetPartner);
@@ -2529,6 +2555,7 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
}
$metrika->setParam(2, 'widgetId', $block->getCode());
$metrika->setParam(3, 'siteId', $this->siteId);
return $metrika;
}

View File

@@ -140,6 +140,42 @@ class View
return 0;
}
/**
* Returns the total number of views (sum of VIEWS field) for the given landing.
* @param int $lid Landing id.
*
* @return int
*/
public static function getNumberTotalViews(int $lid): int
{
$lid = (int)$lid;
$res = ViewTable::getList([
'select' => [
'SUM'
],
'filter' => [
'LID' => $lid
],
'runtime' => [
new Entity\ExpressionField(
'SUM', 'SUM(%s)', ['VIEWS']
)
]
]);
if ($row = $res->fetch())
{
$totalViews = (int)$row['SUM'];
}
if (isset($totalViews) && is_int($totalViews))
{
return $totalViews;
}
return 0;
}
public static function getUniqueUserData(int $lid): array
{
$res = ViewTable::getList([

View File

@@ -1106,7 +1106,7 @@ class Manager
{
if (!defined('LANDING_PREVIEW_URL'))
{
define('LANDING_PREVIEW_URL', self::getRegionPreviewDomain());
define('LANDING_PREVIEW_URL', self::getPreviewDomain());
}
return LANDING_PREVIEW_URL;
@@ -1121,7 +1121,7 @@ class Manager
{
if (!defined('LANDING_PREVIEW_WEBHOOK'))
{
$host = self::getRegionPreviewDomain();
$host = self::getPreviewDomain();
define('LANDING_PREVIEW_WEBHOOK', $host . '/rest/1/gvsn3ngrn7vb4t1m/');
}
@@ -1546,7 +1546,7 @@ class Manager
* Get preview domain based on region.
* @return string
*/
private static function getRegionPreviewDomain(): string
private static function getPreviewDomain(): string
{
$region = Application::getInstance()->getLicense()->getRegion();

View File

@@ -206,6 +206,11 @@ class Component extends \Bitrix\Landing\Node
*/
public static function prepareManifest(Block $block, array $manifest, array &$manifestFull = array())
{
// set predefined
Component::setPredefineForDynamicProps(array(
'LANDING_MODE' => (\Bitrix\Landing\Landing::getEditMode() === true) ? 'Y' : 'N',
));
if (
!isset($manifest['extra']['editable']) ||
!is_array($manifest['extra']['editable'])
@@ -339,6 +344,7 @@ class Component extends \Bitrix\Landing\Node
'original_type' => 'component',
'component_type' => $newExtra[$field]['TYPE'] ?? '',
'attribute' => $field,
'placeholder' => $newExtra[$field]['PLACEHOLDER'] ?? '',
'value' => self::preparePropValue(
$newExtra[$field]['VALUE'],
$fieldItem
@@ -621,7 +627,7 @@ class Component extends \Bitrix\Landing\Node
}
default:
{
$item['placeholder'] = '';
$item['placeholder'] = $item['placeholder'] ?? '';
}
}
break;

View File

@@ -1114,33 +1114,4 @@ class Block
return $result;
}
/**
* Get extensions configs, load relations, load lang phrases
*
* @param array $extCodes - array of extensions codes
* @param array $tplCodes - array of site templates
* @return PublicActionResult - array of assets by type
*/
public static function getAssetsConfig(array $extCodes, array $tplCodes = []): PublicActionResult
{
$result = new PublicActionResult();
$assetsManager = (new Assets\Manager())
->enableSandbox()
->addAsset($extCodes)
;
foreach ($tplCodes as $tpl)
{
$siteTemplatePath =
(defined('SITE_TEMPLATE_PATH') ? SITE_TEMPLATE_PATH : '/bitrix/templates/.default');
$style = $siteTemplatePath . "/template_styles.css";
$assetsManager->addAsset($style);
}
$result->setResult($assetsManager->getOutput());
return $result;
}
}

View File

@@ -239,33 +239,10 @@ class Domain
$siteController = Manager::getExternalSiteController();
if ($siteController)
{
//todo: revert changes after change .by domain
$domainName = $return['domain'];
$byDomainName = '';
$isOnlineSite = str_ends_with($domainName, '.b24site.online');
$isOnlineShop = str_ends_with($domainName, '.b24shop.online');
if ($isOnlineSite)
{
$byDomainName = str_replace('.b24site.online', '.bitrix24site.by', $domainName);
}
if ($isOnlineShop)
{
$byDomainName = str_replace('.b24shop.online', '.bitrix24shop.by', $domainName);
}
$checkResult = $siteController::isDomainExists(
$domainName
$checkResult = $siteController::isDomainExists(
$return['domain']
);
if ($byDomainName === '')
{
$return['available'] = $checkResult < 2;
}
else
{
$checkResultBy = $siteController::isDomainExists(
$byDomainName
);
$return['available'] = $checkResult < 2 && $checkResultBy < 2;
}
$return['available'] = $checkResult < 2;
}
}
catch (SystemException $ex)

View File

@@ -11,6 +11,7 @@ use Bitrix\Landing\Block as BlockCore;
use Bitrix\Landing\TemplateRef;
use Bitrix\Landing\Landing as LandingCore;
use Bitrix\Landing\PublicActionResult;
use Bitrix\Landing\Internals\BlockFavouriteTable;
use Bitrix\Landing\Internals\HookDataTable;
use Bitrix\Landing\History;
use Bitrix\Main\Localization\Loc;
@@ -19,6 +20,14 @@ Loc::loadMessages(__FILE__);
class Landing
{
private const ACTION_ADD = 'add';
private const ACTION_REMOVE = 'remove';
private const STATUS_ADDED = 'added';
private const STATUS_ALREADY_EXISTS = 'already_exists';
private const STATUS_DELETED = 'deleted';
private const STATUS_NOT_FOUND = 'not_found';
/**
* Clear disallow keys from add/update fields.
* @param array $fields
@@ -215,6 +224,10 @@ class Landing
$bad
);
}
if (isset($fields['CATEGORY']))
{
$data['CATEGORY'] = $fields['CATEGORY'];
}
// sort
if (isset($fields['AFTER_ID']))
{
@@ -271,6 +284,139 @@ class Landing
return $result;
}
/**
* Add or remove a block code from the user's list of favourite blocks.
*
* @param string $codeBlock The code of the block to add or remove from favourites.
* @param string $action The action to perform: 'add' to add to favourites, 'remove' to remove from favourites.
*
* @return PublicActionResult Result object with status or error information.
*/
public static function markFavouriteBlock(string $codeBlock, string $action, string $type): PublicActionResult
{
$result = new PublicActionResult();
$userId = Manager::getUserId();
if ($userId <= 0 || !$codeBlock || !in_array($action, [self::ACTION_ADD, self::ACTION_REMOVE], true))
{
$error = new \Bitrix\Landing\Error;
$error->addError('DB_ERROR_ADD','Invalid user, codeBlock or action');
$result->setError($error);
return $result;
}
$existing = BlockFavouriteTable::getList([
'filter' => [
'=USER_ID' => $userId,
'=CODE' => $codeBlock,
],
'select' => ['ID'],
])->fetch();
switch ($action) {
case self::ACTION_ADD:
if (!$existing)
{
$addResult = BlockFavouriteTable::add([
'USER_ID' => $userId,
'CODE' => $codeBlock,
'DATE_CREATE' => new \Bitrix\Main\Type\DateTime(),
]);
if ($addResult->isSuccess())
{
$result->setResult(['status' => self::STATUS_ADDED]);
}
else
{
$error = new \Bitrix\Landing\Error;
$error->addError('DB_ERROR_ADD', $addResult->getErrorMessages());
$result->setError($error);
}
}
else
{
$result->setResult(['status' => self::STATUS_ALREADY_EXISTS]);
}
$metrikaEvent = Metrika\Events::addFavourite;
break;
case self::ACTION_REMOVE:
if ($existing)
{
$deleteResult = BlockFavouriteTable::delete($existing['ID']);
if ($deleteResult->isSuccess())
{
$result->setResult(['status' => self::STATUS_DELETED]);
}
else
{
$error = new \Bitrix\Landing\Error;
$error->addError('DB_ERROR_REMOVE', $deleteResult->getErrorMessages());
$result->setError($error);
}
}
else
{
$result->setResult(['status' => self::STATUS_NOT_FOUND]);
}
$metrikaEvent = Metrika\Events::deleteFavourite;
break;
}
if (isset($metrikaEvent))
{
$metrika = new Metrika\Metrika(Metrika\Categories::getBySiteType($type), $metrikaEvent);
$metrika
->setSection(Metrika\Sections::siteEditor)
->setSubSection('code_' . $codeBlock)
->send()
;
}
return $result;
}
/**
* Returns the list of block codes marked as favourite by the current user.
*
* @return PublicActionResult Result object containing an array of favourite block codes or error information.
*/
public static function getFavouriteBlocks(): PublicActionResult
{
$result = new PublicActionResult();
$userId = Manager::getUserId();
if ($userId <= 0)
{
$error = new \Bitrix\Landing\Error;
$error->addError('INVALID_USER', 'Invalid user');
$result->setError($error);
return $result;
}
$codes = [];
$res = BlockFavouriteTable::getList([
'filter' => [
'=USER_ID' => $userId
],
'select' => ['CODE'],
'order' => ['DATE_CREATE' => 'DESC']
]);
while ($row = $res->fetch())
{
$codes[] = $row['CODE'];
}
$result->setResult($codes);
return $result;
}
/**
* Mark delete or not the block.
* @param int $lid Id of landing.

View File

@@ -74,134 +74,6 @@ class RepoWidget extends Repo
$manifest = Scope\Mainpage::prepareBlockManifest($manifest);
}
// todo: move to non-rest namespace?
/**
* @param int $blockId
* @param array $params
* @return PublicActionResult
*/
public static function fetchData(int $blockId, array $params = []): PublicActionResult
{
$result = new PublicActionResult();
$result->setResult(false);
$error = new Landing\Error;
$block = new Landing\Block($blockId);
if (!$block->getId())
{
$error->addError(
'BLOCK_NOT_FOUND',
Loc::getMessage('LANDING_WIDGET_BLOCK_NOT_FOUND')
);
$result->setError($error);
return $result;
}
if (!Loader::includeModule('rest'))
{
$error->addError(
'REST_NOT_FOUND',
Loc::getMessage('LANDING_WIDGET_REST_NOT_FOUND')
);
$result->setError($error);
return $result;
}
// check app
$repoId = $block->getRepoId();
$app = Landing\Repo::getAppInfo($repoId);
if (
!$repoId
|| empty($app)
|| !isset($app['CLIENT_ID'])
)
{
$error->addError(
'APP_NOT_FOUND',
Loc::getMessage('LANDING_WIDGET_APP_NOT_FOUND')
);
$result->setError($error);
return $result;
}
// check subtype
$manifest = $block->getManifest();
if (
!in_array(self::SUBTYPE_WIDGET, (array)$manifest['block']['subtype'], true)
|| !is_array($manifest['block']['subtype_params'])
|| !isset($manifest['block']['subtype_params']['handler'])
)
{
$error->addError(
'HANDLER_NOT_FOUND',
Loc::getMessage('LANDING_WIDGET_HANDLER_NOT_FOUND_2')
);
$result->setError($error);
return $result;
}
// get auth
$auth = Rest\Application::getAuthProvider()->get(
$app['CLIENT_ID'],
'landing',
[],
Manager::getUserId()
);
if (isset($auth['error']))
{
$error->addError(
'APP_AUTH_ERROR__' . $auth['error'],
$auth['error_description'] ?? ''
);
$result->setError($error);
return $result;
}
$params['auth'] = $auth;
// request
$url = (string)$manifest['block']['subtype_params']['handler'];
$http = new HttpClient();
$data = $http->post(
$url,
$params
);
if ($http->getStatus() !== 200)
{
$error->addError(
'HANDLER_NOT_ALLOW',
Loc::getMessage('LANDING_WIDGET_HANDLER_NOT_ALLOW')
);
$result->setError($error);
return $result;
}
$type = empty($params) ? 'fetch' : 'fetch_params';
UsageStatTable::logLandingWidget($app['CLIENT_ID'], $type);
UsageStatTable::finalize();
if (isset($data['error']))
{
$error->addError(
$data['error'],
$data['error_description'] ?? ''
);
$result->setError($error);
return $result;
}
$result->setResult($data);
return $result;
}
/**
* Enable or disable widgets debug logging
* @param string $appCode

View File

@@ -54,4 +54,14 @@ abstract class Scope
* @return array
*/
abstract public static function getExcludedHooks(): array;
/**
* Check is scope can use extension
* @param string $code - name of extension
* @return bool
*/
public static function isExtensionAllow(string $code): bool
{
return true;
}
}

View File

@@ -1,7 +1,9 @@
<?php
namespace Bitrix\Landing\Site\Scope;
use Bitrix\Landing\Block\BlockRepo;
use Bitrix\Landing\Landing;
use Bitrix\Landing\Role;
use Bitrix\Landing\Manager;
use Bitrix\Landing\Domain;
@@ -15,6 +17,14 @@ use Bitrix\Main\EventManager;
*/
class Mainpage extends Scope
{
private const ALLOWED_EXTENSIONS = [
'landing.widgetvue',
'landing_inline_video',
'landing_carousel',
'landing_jquery',
'landing_icon_fonts',
];
/**
* Method for first time initialization scope.
* @param array $params Additional params.
@@ -29,7 +39,7 @@ class Mainpage extends Scope
$eventManager->addEventHandler(
'landing',
'onBlockRepoSetFilters',
function(Event $event)
function (Event $event)
{
$result = new Entity\EventResult();
$result->modifyFields([
@@ -131,7 +141,8 @@ class Mainpage extends Scope
];
$manifest = array_filter(
$manifest,
function ($key) use ($allowedManifestKeys) {
function ($key) use ($allowedManifestKeys)
{
return in_array(mb_strtolower($key), $allowedManifestKeys);
},
ARRAY_FILTER_USE_KEY
@@ -142,17 +153,12 @@ class Mainpage extends Scope
// not all assets allowed
if (isset($manifest['assets']))
{
$allowedExt = [
'landing.widgetvue',
'landing_inline_video',
'landing_carousel',
];
$manifest['assets'] = [
'ext' => array_filter(
(array)$manifest['assets']['ext'],
function ($item) use ($allowedExt)
static function ($item)
{
return in_array(mb_strtolower($item), $allowedExt);
return in_array(mb_strtolower($item), self::ALLOWED_EXTENSIONS, true);
}
),
];
@@ -171,7 +177,8 @@ class Mainpage extends Scope
];
$manifest['block']['subtype'] = array_filter(
(array)$manifest['block']['subtype'],
function ($item) use ($allowedSubtypes) {
function ($item) use ($allowedSubtypes)
{
return in_array(mb_strtolower($item), $allowedSubtypes);
}
);
@@ -190,7 +197,8 @@ class Mainpage extends Scope
];
$manifest['callbacks'] = array_filter(
(array)$manifest['callbacks'],
function ($item) use ($allowedCallbacks) {
function ($item) use ($allowedCallbacks)
{
return in_array(mb_strtolower($item), $allowedCallbacks);
},
ARRAY_FILTER_USE_KEY
@@ -201,7 +209,7 @@ class Mainpage extends Scope
unset($manifest['callbacks']);
}
}
//unset not allowed style
$allowedStyles = [
//for landing block
@@ -217,7 +225,12 @@ class Mainpage extends Scope
'margin-left',
'margin-right',
'text-align',
'font-size',
'font-family',
'font-weight',
'button',
'text-transform',
'container-max-width',
//for widget
'widget',
'widget-type',
@@ -266,4 +279,14 @@ class Mainpage extends Scope
return $manifest;
}
public static function isExtensionAllow(string $code): bool
{
if (Landing::getEditMode())
{
return true;
}
return in_array($code, self::ALLOWED_EXTENSIONS, true);
}
}

View File

@@ -281,4 +281,19 @@ class Type
return $manifest;
}
/**
* Check is current scope can use extension
* @param string $code - name of extension
* @return bool
*/
public static function isExtensionAllow(string $code): bool
{
if (self::$currentScopeClass !== null)
{
return self::$currentScopeClass::isExtensionAllow($code);
}
return true;
}
}

View File

@@ -75,6 +75,7 @@ class Form
{
$content = self::replaceFormMarkers($content);
}
return $content;
}
@@ -152,9 +153,10 @@ class Form
//add class g-cursor-pointer
preg_match_all('/(class="[^"]*)/i', $matches['pre'], $matchesPre);
$matches['pre'] = str_replace($matchesPre[1][0], $matchesPre[1][0]. ' g-cursor-pointer', $matches['pre']);
$matches['pre'] =
str_replace($matchesPre[1][0], $matchesPre[1][0] . ' g-cursor-pointer', $matches['pre']);
return $script . $matches['pre'] . ' '. $matches['pre2'];
return $script . $matches['pre'] . ' ' . $matches['pre2'];
}
return $matches[0];
@@ -200,7 +202,7 @@ class Form
foreach ($sites as $site)
{
Site::update($site, [
'DATE_MODIFY' => false
'DATE_MODIFY' => false,
]);
}
}
@@ -279,7 +281,7 @@ class Form
$forms[$form['ID']] = $form;
}
}
else if (isset($res['error']))
elseif (isset($res['error']))
{
self::$errors[] = [
'code' => $res['error'],
@@ -330,11 +332,11 @@ class Form
return self::getFormsByFilter(['=IS_CALLBACK_FORM' => 'Y', '=ACTIVE' => 'Y']);
}
protected static function getFormsByFilter(array $filter): array
protected static function getFormsByFilter(array $filter, bool $force = false): array
{
static $cache = [];
$cacheKey = serialize($filter);
if (array_key_exists($cacheKey, $cache))
if (array_key_exists($cacheKey, $cache) && !$force)
{
return $cache[$cacheKey];
}
@@ -343,12 +345,9 @@ class Form
$filter,
static function ($key)
{
$key = trim($key);
$equalKey = ltrim($key, '=');
return
in_array($key, self::AVAILABLE_FORM_FIELDS, true)
|| in_array($equalKey, self::AVAILABLE_FORM_FIELDS, true)
;
$clearKey = preg_replace('/^[^A-Z]*/', '', $key);
return in_array($clearKey, self::AVAILABLE_FORM_FIELDS, true);
},
ARRAY_FILTER_USE_KEY
);
@@ -365,7 +364,8 @@ class Form
$filtred = true;
foreach ($filter as $key => $value)
{
if (!$form[$key] || $form[$key] !== $value)
$clearKey = preg_replace('/[^a-zA-Z0-9]/', '', $key);
if (!$form[$clearKey] || $form[$clearKey] !== $value)
{
$filtred = false;
break;
@@ -431,7 +431,7 @@ class Form
{
$link = '/crm/webform/';
}
else if (Manager::isB24Connector())
elseif (Manager::isB24Connector())
{
$link = '/bitrix/admin/b24connector_crm_forms.php?lang=' . LANGUAGE_ID;
}
@@ -475,8 +475,8 @@ class Form
{
// try to get 1) default callback form 2) last added form 3) create new form
$forms = self::getFormsByFilter([
'=XML_ID' => 'crm_preset_fb'
]);
'=XML_ID' => 'crm_preset_fb',
], true);
$forms = self::prepareFormsToAttrs($forms);
if (empty($forms))
{
@@ -604,11 +604,13 @@ class Form
'attribute' => self::ATTR_FORM_PARAMS,
'type' => 'list',
'items' => !empty(self::$errors)
? array_map(fn ($item) => ['name' => $item['message'], 'value' => false], self::$errors)
: [[
'name' => Loc::getMessage('LANDING_BLOCK_WEBFORM_NO_FORM'),
'value' => false,
]],
? array_map(fn($item) => ['name' => $item['message'], 'value' => false], self::$errors)
: [
[
'name' => Loc::getMessage('LANDING_BLOCK_WEBFORM_NO_FORM'),
'value' => false,
],
],
];
}
@@ -622,7 +624,8 @@ class Form
*/
protected static function prepareFormsToAttrs(array $forms): array
{
$sorted = [];
$callback = [];
$other = [];
foreach ($forms as $form)
{
if (array_key_exists('ACTIVE', $form) && $form['ACTIVE'] !== 'Y')
@@ -637,15 +640,15 @@ class Form
if ($form['IS_CALLBACK_FORM'] === 'Y')
{
$sorted[] = $item;
$callback[] = $item;
}
else
{
array_unshift($sorted, $item);
$other[] = $item;
}
}
return $sorted;
return $callback + $other;
}
// endregion
@@ -678,8 +681,7 @@ class Form
'CONTENT' => '%data-b24form=%',
],
]
)->fetchAll()
;
)->fetchAll();
}
/**
@@ -694,6 +696,7 @@ class Form
{
return (int)$matches[1];
}
return null;
}
@@ -736,7 +739,7 @@ class Form
*/
protected static function createDefaultForm(): array
{
if ($formId = self::createForm([]))
if ($formId = self::createForm(['XML_ID' => 'crm_preset_fb']))
{
return self::getFormsByFilter(['=ID' => $formId]);
}
@@ -754,9 +757,11 @@ class Form
{
$form = new WebForm\Form;
$defaultData = WebForm\Preset::getById('crm_preset_cd');
$xmlId = $formData['XML_ID'] ?? 'crm_preset_cd';
$defaultData['XML_ID'] = '';
$defaultData = WebForm\Preset::getById($xmlId) ?? [];
$defaultData['XML_ID'] = $xmlId;
$defaultData['ACTIVE'] = 'Y';
$defaultData['IS_SYSTEM'] = 'N';
$defaultData['IS_CALLBACK_FORM'] = 'N';
@@ -770,7 +775,7 @@ class Form
$defaultData['AGREEMENT_ID'] = $agreementId;
}
$isLeadEnabled = LeadSettings::getCurrent()->isEnabled();
$isLeadEnabled = LeadSettings::getCurrent()?->isEnabled();
$defaultData['ENTITY_SCHEME'] = (string)(
$isLeadEnabled
? WebForm\Entity::ENUM_ENTITY_SCHEME_LEAD

View File

@@ -205,16 +205,10 @@ class WidgetVue
}
$vueParams = Json::encode($vueParams);
$type = Landing\Site\Scope::getCurrentScopeId();
return "
<script>
(() => {
if (BX.Landing.Env)
{
BX.Landing.Env.getInstance().setType('{$type}');
}
const init = () => {
(new BX.Landing.WidgetVue(
{$vueParams}

View File

@@ -926,9 +926,9 @@ class Landing
protected static function prepareAdditionalFields(array $data, array $additional, array $ratio = null): array
{
$data['ADDITIONAL_FIELDS']['THEME_USE'] = 'N';
if (isset($additional['theme']) || isset($additional['theme_use_site']))
if (isset($additional['theme']))
{
$color = $additional['theme_use_site'] ?? $additional['theme'];
$color = $additional['theme'];
if ($color[0] !== '#')
{
$color = '#'.$color;
@@ -938,7 +938,7 @@ class Landing
// for variant if import only page in existing site
$isSinglePage = !is_array($ratio) || empty($ratio);
if ($isSinglePage && !$additional['theme_use_site'])
if ($isSinglePage)
{
$data['ADDITIONAL_FIELDS']['THEME_USE'] = 'Y';
}
@@ -1197,7 +1197,9 @@ class Landing
{
if (array_key_exists('data-end-date', $attrItem))
{
$neededAttr = $attrItem['data-end-date'] / 1000;
$neededAttr = is_numeric($attrItem['data-end-date'])
? (int)$attrItem['data-end-date'] / 1000
: 0;
$currenDate = time();
if ($neededAttr < $currenDate)
{

View File

@@ -1,117 +0,0 @@
<?php
namespace Bitrix\Landing\Zip\Nginx;
use \Bitrix\Main\Context;
use \Bitrix\Main\Loader;
class Archive
{
/**
* Archive name.
* @var string
*/
protected $name;
/**
* Archive Entries.
* @var ArchiveEntry[]
*/
protected $entries = [];
/**
* Archive constructor.
* @param string $name Archive name.
*/
public function __construct($name)
{
$this->name = $name;
}
/**
* Add one entry. in current archive.
* @param ArchiveEntry $archiveEntry Entry for archive.
*/
public function addEntry($archiveEntry)
{
if ($archiveEntry instanceof ArchiveEntry)
{
$this->entries[] = $archiveEntry;
}
}
/**
* Returns true if the archive does not have entries.
* @return bool
*/
public function isEmpty()
{
return empty($this->entries);
}
/**
* Return entries as string.
* @return string
*/
protected function getFileList()
{
$list = [];
foreach ($this->entries as $entry)
{
$list[] = (string)$entry;
}
unset($entry);
return implode("\n", $list);
}
/**
* Add necessary headers.
* @return void
* @throws \Bitrix\Main\ArgumentNullException
*/
protected function addHeaders()
{
$httpResponse = Context::getCurrent()->getResponse();
$httpResponse->addHeader('X-Archive-Files', 'zip');
$utfName = \CHTTP::urnEncode($this->name, 'UTF-8');
$translitName = \CUtil::translit($this->name, LANGUAGE_ID, [
'max_len' => 1024,
'safe_chars' => '.',
'replace_space' => '-',
]);
$httpResponse->addHeader(
'Content-Disposition',
"attachment; filename=\"" . $translitName . "\"; filename*=utf-8''" . $utfName
);
unset($utfName, $translitName, $httpResponse);
}
/**
* Sends content to output stream and sets necessary headers.
* @return void
*/
public function send()
{
if (!$this->isEmpty())
{
$this->disableCompression();
$this->addHeaders();
Context::getCurrent()->getResponse()->flush(
$this->getFileList()
);
}
}
/**
* Disable compression of module compression.
*/
protected function disableCompression()
{
if (Loader::includeModule('compression'))
{
\CCompress::disableCompression();
}
}
}

View File

@@ -1,173 +0,0 @@
<?php
namespace Bitrix\Landing\Zip\Nginx;
use \Bitrix\Main\Text\Encoding;
use \Bitrix\Main\Config\Option;
use \Bitrix\Landing\File;
use \Bitrix\Landing\Manager;
class ArchiveEntry
{
/**
* File name in entry.
* @var string
*/
protected $name;
/**
* File path in entry.
* @var string
*/
protected $path;
/**
* File id.
* @var string
*/
protected $fileId;
/**
* File size in entry.
* @var string
*/
protected $size;
/**
* Entry constructor.
*/
protected function __construct()
{
$this->fileId = 0;
}
/**
* Creates Entry from file path.
* @param string $filePath File id from b_file.
* @return static
*/
public static function createFromFilePath($filePath)
{
$fileArray = \CFile::MakeFileArray($filePath);
if ($fileArray)
{
return self::createFromFile([
'ID' => 0,
'ORIGINAL_NAME' => $fileArray['name'],
'FILE_SIZE' => $fileArray['size'],
'SRC' => substr(
$fileArray['tmp_name'],
strlen(Manager::getDocRoot())
),
]);
}
return null;
}
/**
* Creates Entry from file id (from b_file).
* @param int $fileId File id from b_file.
* @return static
*/
public static function createFromFileId($fileId)
{
$fileArray = File::getFileArray($fileId);
if (
!$fileArray ||
empty($fileArray['SRC'])
)
{
return null;
}
return self::createFromFile($fileArray);
}
/**
* Creates Entry from file array.
* @param array $fileArray File id from b_file.
* @return static
*/
protected static function createFromFile(array $fileArray)
{
$zipEntry = new static;
$zipEntry->name = $fileArray['ORIGINAL_NAME'];
$zipEntry->fileId = $fileArray['ID'];
$zipEntry->size = $fileArray['FILE_SIZE'];
$fromClouds = false;
$filename = $fileArray['SRC'];
if (isset($fileArray['HANDLER_ID']) && !empty($fileArray['HANDLER_ID']))
{
$fromClouds = true;
}
unset($fileArray);
if ($fromClouds)
{
$filename = preg_replace('~^(http[s]?)(\://)~i', '\\1.' , $filename);
$cloudUploadPath = Option::get(
'main',
'bx_cloud_upload',
'/upload/bx_cloud_upload/'
);
$zipEntry->path = $cloudUploadPath . $filename;
unset($cloudUploadPath);
}
else
{
$zipEntry->path = self::encodeUrn(
Encoding::convertEncoding($filename, LANG_CHARSET, 'UTF-8')
);
}
unset($filename);
return $zipEntry;
}
/**
* Encodes uri: explodes uri by / and encodes in UTF-8 and rawurlencodes.
* @param string $uri Uri.
* @return string
*/
protected function encodeUrn($uri)
{
$result = '';
$parts = preg_split(
"#(://|:\\d+/|/|\\?|=|&)#", $uri, -1, PREG_SPLIT_DELIM_CAPTURE
);
foreach ($parts as $i => $part)
{
$part = Manager::getApplication()->convertCharset(
$part,
LANG_CHARSET,
'UTF-8'
);
$result .= ($i % 2)
? $part
: rawurlencode($part);
}
unset($parts, $i, $part);
return $result;
}
/**
* Returns representation zip entry as string.
* @return string
*/
public function __toString()
{
$name = Encoding::convertEncoding(
$this->name,
LANG_CHARSET,
'UTF-8'
);
return "- {$this->size} {$this->path} /upload/{$this->fileId}/{$name}";
}
}

View File

@@ -1,24 +0,0 @@
<?php
namespace Bitrix\Landing\Zip\Nginx;
use \Bitrix\Landing\Manager;
use \Bitrix\Main\ModuleManager;
class Config
{
/**
* Enable or not main option.
* @return bool
*/
public static function serviceEnabled()
{
if (ModuleManager::isModuleInstalled('bitrix24'))
{
return true;
}
else
{
return Manager::getOption('enable_mod_zip', 'N') == 'Y';
}
}
}