Чиним ошибку 400 Bad Request с помощью mod_rpaf у BitrixEnv

, Михаил

Воспользуемся замечательнейшим модулем https://github.com/gnif/mod_rpaf который протестирован и успешно эксплуатируется в на многих серверах

Собираем модуль:

yum groupinstall "Development Tools"
yum install httpd-devel
wget -O /tmp/mod_rpaf.c https://raw.githubusercontent.com/gnif/mod_rpaf/stable/mod_rpaf.c
apxs -c -i /tmp/mod_rpaf.c

Далее создаём /etc/httpd/bx/custom/rpaf.conf

LoadModule              rpaf_module modules/mod_rpaf.so
RPAF_Enable             On
RPAF_ProxyIPs           127.0.0.1 Ваш.IP.Сервера
RPAF_SetHostName        On
RPAF_SetHTTPS           On
RPAF_SetPort            On
RPAF_ForbidIfNotProxy   Off

И не забываем выключить remoteip в файле /etc/httpd/conf.modules.d/00-base.conf и удалить файл /etc/httpd/bx/conf/mod_rpaf.conf

На выходе получим

[root@divasoft ~]# apachectl -M | grep -E 'remoteip|rpaf'
 rpaf_module (shared)

Перезапускаем httpd


В файле /etc/nginx/bx/site_enabled/ssl.s1.conf меняем строки

proxy_set_header   Host   $host:443;
proxy_set_header   HTTPS   YES;

На

proxy_set_header   Host   $host;
proxy_set_header   X-Forwarded-Proto   $scheme;
proxy_set_header   X-Forwarded-Port   $server_port;

В файле /etc/nginx/bx/site_enabled/s1.conf меняем строки

proxy_set_header   Host   $host:80;

На

proxy_set_header   Host   $host;
proxy_set_header   X-Forwarded-Proto   $scheme;
proxy_set_header   X-Forwarded-Port   $server_port;

Перезапускаем nginx

Итоговый блок будет выглядеть как

proxy_set_header   X-Real-IP   $remote_addr;
proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_set_header   Host   $host;
proxy_set_header   X-Forwarded-Proto   $scheme;
proxy_set_header   X-Forwarded-Port   $server_port;

Приём заказов в 1С из Битрикс с нескольких сайтов

, Михаил

Существует проблема с выгрузкой заказов с 2 разных сайтов.

Если сайты являются копиями друг друга, т.е. сначала был сделан сайт 1, затем скопирован и развернут с базой, и на его основе с незначительными доработками был сделан сайт 2. Для 1 сайта уже была подключены импорты заказов из Битрикс, и импорт номенклатуры в Битрикс. Для второго сайта сделали идентичные обмены, также путем копирования обменов, и изменения параметров отбора и древа групп в обмене товарами, и без изменений обмена заказами, кроме url адреса. При включении на автомате обоих импортов заказов происходит проблема дублирования заказа. Со 2 сайта приходит заказ, и полностью удаляет документы старого заказа в 1С, полученного с первого сайта. При сравнении и проверке заказов выяснилось, что заказы с идентичными ID.

Самый быстрый - меняем стартовое значение поля ID
Оно int(11), значит у нас есть в запасе от (-2147483648 до 2147483647)

Для первого сайта оставляем всё как есть.
Для второго:
ALTER TABLE `b_sale_order` AUTO_INCREMENT = 100000000; Для третьего:
ALTER TABLE `b_sale_order` AUTO_INCREMENT = 200000000;

Для пользователя - задействуем нумератор заказов, что бы не боялся больших цифр.

Чиним fastdownload у nginx в bitrixenv для Яндекс Cloud Storage

, Михаил

Решил все загружаемые файлы в наш Битрикс24 отправить в Яндекс.Облако

Но файлы просто не скачивались, после долгой отладки нашёл как отдаются файлы, копнул глубже... ядро Битрикс, используя заголовок x-accel-redirect, делает всю эту магию с внешними хранилищами.
Находим секцию в /etc/nginx/bx/conf/bitrix_general.conf

# Use nginx to return static content from s3 cloud storage
# /upload/bx_cloud_upload/...amazonaws.com/
location ^~ /upload/bx_cloud_upload/ {

И туда добавляем

    location ~ ^/upload/bx_cloud_upload/(http[s]?)\.([^/:\s]+)\.storage\.yandexcloud\.net/([^\s]+)$ {
        internal;
        resolver 8.8.8.8;
        proxy_method GET;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Server $host;
        #proxy_max_temp_file_size 0;
        proxy_pass $1://$2.storage.yandexcloud.net/$3;
    }

Как в Битриксе, на странице оформления заказа, раскрыть все блоки

, Михаил

Берём за основу стандартный .default шаблон компонента sale.order.ajax, работаем в файле order_ajax.js

  1. Включаем редактирование блоков «Регион» и «Пользователь»: комментируем строки /*if (this.activeSectionId !== this.regionBlockNode.id) this.editFadeRegionContent(this.regionBlockNode.querySelector('.bx-soa-section-content')); if (this.activeSectionId != this.propsBlockNode.id) this.editFadePropsContent(this.propsBlockNode.querySelector('.bx-soa-section-content'));*/
  2. Удаляем кнопки «Далее» и «Назад»: комментируем строки /*node.appendChild( BX.create('DIV', { props: {className: 'row bx-soa-more'}, children: [ BX.create('DIV', { props: {className: 'bx-soa-more-btn col-xs-12'}, children: buttons }) ] }) );*/
  3. Все блоки раскрываем: меняем строку var active = section.id == this.activeSectionId На строку var active = true,
  4. Удаляем обработчики при клике на заголовки: комментируем строки /*BX.unbindAll(titleNode); if (this.result.SHOW_AUTH) { BX.bind(titleNode, 'click', BX.delegate(function(){ this.animateScrollTo(this.authBlockNode); this.addAnimationEffect(this.authBlockNode, 'bx-step-good'); }, this)); } else { BX.bind(titleNode, 'click', BX.proxy(this.showByClick, this)); editButton = titleNode.querySelector('.bx-soa-editstep'); editButton && BX.bind(editButton, 'click', BX.proxy(this.showByClick, this)); }*/
  5. Удаляем ссылки «Изменить»: в конец функции editOrder добавляем код var editSteps = this.orderBlockNode.querySelectorAll('.bx-soa-editstep'), i; for (i in editSteps) { if (editSteps.hasOwnProperty(i)) { BX.remove(editSteps[i]); } }

Находим файл из Битрикс24.Диск по ID

, Михаил

Нельзя просто так взять и получить настоящий файл для дальнейшей манипуляции с ним. В api такого нет. ORM d7 в помощь


<?php 
function getRealFileFromDiskById($diskId) {
	    \Bitrix\Main\Loader::includeModule('disk');
	    $resObjects = \Bitrix\Disk\Internals\ObjectTable::getList([
	            'select' => ['NAME''FILE_ID'],
	            'filter' => [
	                '=ID' => $diskId,
	            ]
	    ]);
	    if ($arObject $resObjects->fetch()) {
		        $arObject['PATH'] = CFile::GetPath($arObject['FILE_ID']);
		        $arObject['FULL_PATH'] = $_SERVER['DOCUMENT_ROOT'].$arObject['PATH'];
		        return $arObject;
		}
	    return false;
	}

?>

A.I.Divasoft - Пишем бота для Slack, создаём репозиторий в Bitbucket и оповещаем о коммитах в группе Битрикс24

, Михаил

Продолжаю оптимизировать рабочий процесс. Теперь создание репозитория, привязка оповещений в Битрикс24 из Bitbucket, к выбранной группе становится гораздо быстрее!

A.I.Divasoft - репозиторий

A.I.Divasoft - Пишем бота для Slack, создаём связанный контакт с компанией, генерируем лид/сделку, создаём или находим группу, приглашаем в группу

, Михаил

Куча действий связанных с новым клиентом, за пару кликов - новая функция нашего бота!

A.I.Divasoft - приглашаем клиента

A.I.Divasoft - Пишем бота для Slack, добавляем задачи в Битрикс24

, Михаил

Первым делом научили делать задачу в наш Битрикс24!

Текст задачи может формироваться сразу из сообщения в чате, постановщик - тот кто пишет, ответственный запоминается, как и выбор проектов.

И очень важная фишка - добавляем в наблюдатели к задаче всех пользователей экстранет относящихся к этой группе, либо без наблюдателей.

A.I.Divasoft - ставим задачу в Битрикс 24

Устанавливаем заголовки в элементе/разделе из SEO модуля Битрикс

, Михаил

Добавляем в result_modifier.php


<?php 

// /local/templates/.default/components/bitrix/catalog.section/.default/result_modifier.php
// для элемента используем $ipropValues = new \Bitrix\Iblock\InheritedProperty\ElementValues($IBLOCK_ID,$ELEMENT_ID);
$ipropValues = new \Bitrix\Iblock\InheritedProperty\SectionValues($arResult['IBLOCK_ID'], $arResult['ID']);
$SEO $ipropValues->getValues();
$cp $this->__component;
if (is_object($cp)) {
	    $cp->arResult['SEO'] = $SEO;
	    $cp->SetResultCacheKeys(array('SEO'));
	    $arResult['SEO'] = $cp->arResult['SEO'];
	}
?>

Добавляем в component_epilog.php


<?php 

// /local/templates/.default/components/bitrix/catalog.section/.default/component_epilog.php
global $APPLICATION;
// DIVASOFT
if ($arResult['SEO']['SECTION_META_TITLE']) {
	    $APPLICATION->SetPageProperty("title"$arResult['SEO']['SECTION_META_TITLE']);
	}
if ($arResult['SEO']['SECTION_META_DESCRIPTION']) {
	    $APPLICATION->SetPageProperty("description"$arResult['SEO']['SECTION_META_DESCRIPTION']);
	}
if ($arResult['SEO']['SECTION_META_KEYWORDS']) {
	    $APPLICATION->SetPageProperty("keywords"$arResult['SEO']['SECTION_META_KEYWORDS']);
	}
if ($arResult['SEO']['SECTION_PAGE_TITLE']) {
	    $APPLICATION->SetTitle($arResult['SEO']['SECTION_PAGE_TITLE']);
	}
?>

Обновляем BitrixEnv до 7.3.4

, Михаил

Перед всеми манипуляциями делаем бэкап/снапшот.

Обновляем ядро/модули Bitrix до последних стабильных обновлений или бета-версий.

Потом обязательно добавляем репозиторий, нужен для обновления mysql до версии 5.7, или будете откатываться при потере базы во время обновления

yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm

Или временный фикс /etc/yum.repos.d/percona-release.repo - отключаем проверку ключа

[percona-release-noarch]
name = Percona-Release YUM repository - noarch
baseurl = http://repo.percona.com/release/...
enabled = 1
gpgcheck = 0
gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Percona

Повышаем версию php с 5.6 до версии 7.1

Задаем пароль root mysql через меню, или вручную в файле /root/.my.cnf

Повышаем версию mysql с 5.5 до версии 5.7

Диверсия на сайте клиентов от сторонних разработчиков

, Михаил

Пишет нам один заказчик, что сайт начал долго открываться.
Заходим - действительно, ~17 секунд отдаётся контент.
Начали анализировать ситуацию, в одном из включаемых файлов видим это:

sleep15.png

sleep(15); - команда которая говорит серверу - подожди просто 15 секунд, потом делай свои дела дальше.

Это даже не смешно, "случайно" такую команду не напишешь, за 15 лет разработки я применил эту команду только 1 раз, и то, в сервисном скрипте, который по крону делает что то.

Так что это 100% диверсия. И те разработчики, если такое сделали, не известно, на что ещё способны. Как минимум 2 бэкдора уже нашёл.

Возвращаем информацию об удаляющихся при обмене с 1С оплате и доставке в админке Битрикса

, Михаил

Продолжение этой проблемы. Когда менеджер работает одновременно в админке и в 1С, и когда в 1С выключена загрузка отгрузок и оплат - Битрикс убирает из заказа эти данные. Возвращаем информацию в заказ, и список заказов.


<?php 

// local/php_interface/init.php
// Добавляем информацию внутри заказа
\Bitrix\Main\EventManager::getInstance()->addEventHandler('sale''onSaleAdminOrderInfoBlockShow', ['DivasoftFixSyncOrderInfo''onSaleAdminOrderInfoBlockShow']);
// Заполняем колонки в списке заказов
\Bitrix\Main\EventManager::getInstance()->addEventHandler("main",  "OnAdminListDisplay", ['DivasoftFixSyncOrderInfo''onAdminListDisplay']);
\Bitrix\Main\EventManager::getInstance()->addEventHandler("main",  "OnAdminSubListDisplay", ['DivasoftFixSyncOrderInfo''onAdminListDisplay']);
class DivasoftFixSyncOrderInfo {
	    static function getSystemDeliveryNameByOrderD7($order) {
		        $shipmentCollection $order->getShipmentCollection();
		        $shipmenName "Не выбрана";
		        $systemShipmentItemCollection $shipmentCollection->getSystemShipment()->getShipmentItemCollection();
		        foreach ($shipmentCollection as $obShipment) {
			            if ($obShipment->isSystem()) {
				                $arShipment $obShipment->getFields()->getValues();
				                $shipmenName $arShipment['DELIVERY_NAME'];
				}
			}
		        return $shipmenName;
		}
	    static function getSystemPaymentNameByOrderD7($order) {
		        // getSystemPayment такого метода нет, запросим информацию по тому что есть
		        $paySystemService = \Bitrix\Sale\PaySystem\Manager::getObjectById($order->getField('PAY_SYSTEM_ID'));
		        $payName $paySystemService->getField("NAME");
		        return $payName;
		}
	    function onSaleAdminOrderInfoBlockShow(\Bitrix\Main\Event $event) {
		        $order $event->getParameter("ORDER");
		        $shipmenName self::getSystemDeliveryNameByOrderD7($order);
		        $payName self::getSystemPaymentNameByOrderD7($order);
		        return new \Bitrix\Main\EventResult(
		            \Bitrix\Main\EventResult::SUCCESS, array(
		            array('TITLE' => 'Доставка:''VALUE' => $shipmenName'ID' => 'dvs_system_shipment'),
		            array('TITLE' => 'Оплата:''VALUE' => $payName'ID' => 'dvs_system_payment'),
		            ), 'sale'
		        );
		}
	    function onAdminListDisplay(&$list) {
		        if ($list->table_id == "tbl_sale_order") {
			            foreach ($list->aRows as &$row) {
				                foreach ($row->aFields as $key => &$val) {
					                    $order false;
					                    if ($key == "DELIVERY") {
						                        if (!$val['view']['value']) {
							                            if (!$order) {
								                                $order = \Bitrix\Sale\Order::load($row->arRes['ID']);
								}
							                            $val['view']['value'] = self::getSystemDeliveryNameByOrderD7($order);
							}
						}
					                    if ($key == "PAY_SYSTEM") {
						                        if (!$val['view']['value']) {
							                            if (!$order) {
								                                $order = \Bitrix\Sale\Order::load($row->arRes['ID']);
								}
							                            $val['view']['value'] = self::getSystemPaymentNameByOrderD7($order);
							}
						}
					}
				}
			}
		}
	}
?>

Опасность архивирования заказов в Битрикс, или почему увеличиваются остатки на складе

, Михаил

Столкнулись с проблемой. Остатки у некоторых товаров сами увеличиваются. Резервирование выключено, заказы не отменяются.

В итоге, обратил внимание на "Архивирование заказов". Долго описывал проблему, в итоге тех.поддержка ответила:

Посмотрели по заказу у отгрузки стоит RESERVED=Y, но сам заказ и отгрузки по нему не отгружались, поэтому перед архивацией произошло разрезервация остатков. Такая же логика и при удалении заказа, если отгрузка не отгружалась, то резервация возвращается обратно. А архивирование по своему поведению совпадает с удалением.
Это штатное поведение функционала резервирования и архивирования.

Штатное поведение Карл! Выключаем архивацию, радуемся правильным остаткам на складе.

Автозапуск Sypex Dumper по ссылке

, Михаил

Sypex Dumper можно запускать по cron, но нельзя запустить вручную сохранённую задачу.
Добавляем на самую первую строчку index.php

if (isset($_GET['startjob']) && $_GET['startjob']=="Y") { $argc=2; $argv=["index.php","-j=job_name"]; }

И теперь по ссылке можно запускать предустановленную задачу.

Отлючаем bootstrap.css у 1С-Битрикс

, Михаил

Добавляем в init.php обработчик и у нас нет встроенного бутстрапа, даже если включить объединение стилей - тоже сработает. т.к. файлы ядра не добавляются в единый файл


<?php 
\Bitrix\Main\EventManager::getInstance()->addEventHandler("main""OnEndBufferContent""deleteKernelCss");
function deleteKernelCss(&$content) {
	    global $USER$APPLICATION;
	    if(strpos($APPLICATION->GetCurDir(), "/bitrix/")!==false) return;
	    if($APPLICATION->GetProperty("save_kernel") == "Y") return;
	    $arPatternsToRemove = Array(
	        '/<link.+?href=".+?bitrix\/css\/main\/bootstrap.css[^"]+"[^>]+>/',
	    );
	    $content preg_replace($arPatternsToRemove""$content);
	    $content preg_replace("/\n{2,}/""\n\n"$content);
	}
?>

Запрещаем удаление оплат и отгрузок из заказов Битрикса при синхронизации с 1С

, Михаил

Уже долгое время компания 1С-Битрикс сделала полную синхронизацию заказа 1С и БУС, что черевато ситуацией - удаляются отгрузки и оплаты

Временное решение данной проблемы init.php:


<?php 

use \Bitrix\Main\EventManager;
use \Bitrix\Main\Event;
use \Bitrix\Main\Entity;
use \Bitrix\Sale\Order;
use \Bitrix\Sale\Payment;
use \Bitrix\Sale\PaySystem\Manager;
use \Bitrix\Sale\Shipment;
use \Bitrix\Sale\Helpers\Admin\Blocks\OrderBasketShipment;
$inst EventManager::getInstance();
$inst-> addEventHandler('sale''OnBeforeCollectionDeleteItem''saveInfo');
$inst-> addEventHandler('sale''OnSaleOrderBeforeSaved''reverseInfo');
//Небольшая прослойка, возвращает доступные поля
/**
 * @param array $arValues
 * @param array $allowedFields
 * @return array $result
 */
function checkFields$arValues$allowedFields) {
	   $result = array();
	   foreach ( $arValues as $key => $value ) {
		      if ( in_array$key,$allowedFields ) && !in_array($key, array('ACCOUNT_NUMBER')) ) {
			         $result[$key] = $value;
			}
		}
	   return $result;
	}
function saveInfo(\Bitrix\Main\Event $event ) {
	   /**
	    * @var \Bitrix\Sale\Shipment|\Bitrix\Sale\Payment $entity
	    */
	   if ( $_SESSION['BX_CML2_EXPORT'] ) {
		      $entity $event->getParameter('ENTITY');
		      if ( $entity instanceof Shipment ) {
			         if ( !is_array$_SESSION['BX_CML2_EXPORT']['DELETED_SHIPMENTS'] )  )
			            $_SESSION['BX_CML2_EXPORT']['DELETED_SHIPMENTS'] = array();
			         if ( !$entity->isSystem() )
			            $_SESSION['BX_CML2_EXPORT']['DELETED_SHIPMENTS'][] = checkFields$entity->getFields()->getValues(), Shipment::getAvailableFields() );
			}
		      if ( $entity instanceof Payment ) {
			         if ( !is_array$_SESSION['BX_CML2_EXPORT']['DELETED_PAYMENTS'] )  )
			            $_SESSION['BX_CML2_EXPORT']['DELETED_PAYMENTS'] = array();
			         $_SESSION['BX_CML2_EXPORT']['DELETED_PAYMENTS'][] = checkFields$entity->getFields()->getValues(), Payment::getAvailableFields() );
			}
		}
	   else {
		      return;
		}
	}
function reverseInfo(\Bitrix\Main\Event $event ) {
	   /**
	    * @var \Bitrix\Sale\Order $order
	    * @var \Bitrix\Sale\ShipmentCollection $shipmentCollection
	    * @var \Bitrix\Sale\Shipment $shipment
	    * @var \Bitrix\Sale\PaymentCollection $paymentCollection
	    * @var \Bitrix\Sale\Payment $payment
	    * @var \Bitrix\Sale\PropertyValue $somePropValue
	    * **/
	   if ( $_SESSION['BX_CML2_EXPORT'] ) {
		      $order $event->getParameter("ENTITY");
		      if ( $_SESSION['BX_CML2_EXPORT']['DELETED_SHIPMENTS'] ) {
			         //Вернем отгрузки
			         $shipmentCollection $order->getShipmentCollection();
			         $systemShipmentItemCollection $shipmentCollection->getSystemShipment()->getShipmentItemCollection();
			$products = array();
			         $basket $order->getBasket();
			         if ($basket)
			         {
				            /** @var \Bitrix\Sale\BasketItem $product */
				            $basketItems $basket->getBasketItems();
				            foreach ($basketItems as $product)
				            {
					               $systemShipmentItem $systemShipmentItemCollection->getItemByBasketCode($product->getBasketCode());
					               if ($product->isBundleChild() || !$systemShipmentItem || $systemShipmentItem->getQuantity() <= 0)
					                  continue;
					               $products[] = array(
					                  'AMOUNT' => $product->getQuantity(),
					                  'BASKET_CODE' => $product->getBasketCode()
					               );
					}
				}
			         /** @var \Bitrix\Sale\Shipment $obShipment */
			         /** @var array $shipmentFields */
			         foreach ( $_SESSION['BX_CML2_EXPORT']['DELETED_SHIPMENTS'] as $shipmentFields ) {
				            $fg true;
				            foreach( $shipmentCollection as $obShipment ) {
					               if ($obShipment->isSystem())
					                  continue;
					               $usedFields checkFields($obShipment->getFields()->getValues(), Shipment::getAvailableFields() );
					               if ( countarray_diff_assoc$shipmentFields$usedFields) ) == )
					                  $fg false;
					 //доставка с такими полями уже есть
					}
				            if ( $fg ) {
					               $shipment $shipmentCollection->createItem();
					               $shipment->setFields$shipmentFields );
					               OrderBasketShipment::updateData($order$shipment$products);
					}
				}
			         unset( $_SESSION['BX_CML2_EXPORT']['DELETED_SHIPMENTS'] );
			}
		      if ( $_SESSION['BX_CML2_EXPORT']['DELETED_PAYMENTS'] ) {
			         //Вернем оплаты
			         $paymentCollection $order->getPaymentCollection();
			         /** @var \Bitrix\Sale\Payment $obPayment */
			         /** @var array $paymentFields */
			         foreach ( $_SESSION['BX_CML2_EXPORT']['DELETED_PAYMENTS'] as $paymentFields ) {
				            $fg true;
				            foreach( $paymentCollection as $obPayment ) {
					               $usedFields checkFields$obPayment->getFields()->getValues(), Payment::getAvailableFields() );
					               if ( countarray_diff_assoc$paymentFields$usedFields) ) == )
					                  $fg false;
					 //такая оплата уже есть
					}
				            if ( $fg ) {
					               $payment $paymentCollection->createItem();
					               $payment->setFields$paymentFields );
					}
				}
			         unset( $_SESSION['BX_CML2_EXPORT']['DELETED_PAYMENTS'] );
			}
		      //Проверим сумму заказа
		      $paymentCollection $order->getPaymentCollection();
		      if ( ($sumP $paymentCollection->getSum() ) != ($sumO $order->getPrice() ) ) {
			         $diff $sumO $sumP;
			         $innerPayID Manager::getInnerPaySystemId();
			         foreach ( $paymentCollection as $payment ) {
				            if ( $payment->getPaymentSystemId() != $innerPayID) {
					               $newVal floatval($payment->getField("SUM")) + floatval($diff);
					               $payment->setField("SUM"$newVal);
					}
				}
			}
		}
	}

?>

Находим актуальную версию Flashphoner WebSDK 2.0

, Михаил

Сложно найти в этом огромном списке актуальную версию сборки. Теперь с помощью небольшого парсера получаем свежую дату и название файла для скачивания.

Результат можно посмотреть тут


<?php 

    public function getListUpdates() {
	        $html file_get_contents('https://flashphoner.com/downloads/builds/flashphoner_client/wcs_api-2.0/');
	        $build [];
	        $doc = new DOMDocument();
	        $doc->loadHTML($html);
	        $xpath = new DOMXpath($doc);
	        $xPathText $xpath->query('//table//tr');
	        foreach ($xPathText as $row) {
		            $a $xpath->query('.//td[2]/a'$row);
		            $date $xpath->query('.//td[3]'$row);
		            if (trim($a->item(0)->nodeValue) != "Parent Directory" && trim($date->item(0)->nodeValue) != "") {
			                $build[trim($date->item(0)->nodeValue)] = trim($a->item(0)->nodeValue);
			            }
		        }
	        uksort($build"cmpDate");
	        reset($build);
	        return ["date" => key($build), "file" => current($build)];
	    }

?>

И функция для сравнения двух дат


<?php 
function cmpDate($a$b) {
	    $cA strtotime($a);
	    $cB strtotime($b);
	    if ($cA == $cB) {
		        return 0;
		    } else {
		        return ($cA $cB) ? : -1;
		    }
	}
?>

Потом скачиваем и распаковываем наш архив, выбираем файлы для обновления - копируем в рабочую папку.


<?php 

        file_put_contents($this->updateFilefile_get_contents($this->urlUpdate $this->freshBuild['file']));
        // Распаковываем обновления
        exec("tar -zxvf {$this->updateFile} -C {$this->archive_dir}");
        // Определяем набор файлов для обновления
        $findFirst glob($this->archive_dir "/*"GLOB_ONLYDIR);
        reset($findFirst);
        $unpackedDir current($findFirst);
        $filesUpdate = [
            "$unpackedDir/examples/demo/dependencies/websocket-player/video-worker2.js" => "$this->pathToUpdate/video-worker2.js",
            "$unpackedDir/examples/demo/dependencies/websocket-player/WSReceiver2.js" => "$this->pathToUpdate/WSReceiver2.js",
            "$unpackedDir/examples/demo/dependencies/js/utils.js" => "$this->pathToUpdate/utils.js",
            "$unpackedDir/media-provider.swf" => "$this->pathToUpdate/media-provider.swf",
            "$unpackedDir/flashphoner.min.js" => "$this->pathToUpdate/flashphoner.min.js",
            "$unpackedDir/flashphoner.js" => "$this->pathToUpdate/flashphoner.js",
             // С player.js осторожнее, т.к. в нём изменения, потом сравниваем в редакторе
            "$unpackedDir/examples/demo/streaming/embed_player/player.js" => "$this->pathToUpdate/player.upd.js",
        ];
        // Копируем обновлённые файлы
        foreach ($filesUpdate as $fileFrom => $fileTo) {
	            copy($fileFrom$fileTo);
	        }

?>

Важное уведомление от 1С-Битрикс

, Михаил

С 1 января 2018 года будет ограничена поддержка 1С-Битрикс на PHP версии ниже 5.6.
Обратитесь в компанию Дивасофт, мы поможем перейти на подходящую версию PHP!

Рекомендации по выбору виртуального сервера (VPS) для 1С-Битрикс

, Михаил

Мы перепробовали много хостинговых компаний.
Самый выгодный и удобный оказался Flops.

В частности тариф "Оплата за потребление" - зачем платить постоянно за полную мощность, когда есть вариант платить только за потреблённые ресурсы.
Так же есть возможность быстро добавить или убрать оперативную память, добавить или убрать ядра процессоров, расширить место на SSD диске.

Все действия без доплаты, в течении нескольких секунд. Можно сделать в 1 клик клон сервера, потом его быстро удалить. Конечно не amazon, но в РФ это очень интересный вариант!

Исправляем ошибку 400 Bad Request при включении https у 1С-Битрикс

, Михаил

После включение редиректа на https, в некоторых случаях появляется ошибка 400 Bad Request The plain HTTP request was sent to HTTPS port

Всё происходит из за mod_dir, он берет на себя редирект с папки без слеша на папку с слешом, но он не воспринимает "HTTPS on" как побудитель использования схемы https:// 

Что бы всё это заработало, нужно:

  • В конфигах nginx'a ничего не трогаем
    proxy_set_header       Host       $host:443;
  • В конфиге апача который отвечает за ваш домен
    Если у вас конфигурация многосайтовая - /etc/httpd/bx/conf/bx_ext_site.local.conf
    односайтовая - /etc/httpd/bx/conf/default.conf
    К названию сервера ServerName  site.local  дописываем:
    ServerName  https://site.local 

смысл следующий: http://httpd.apache.org/docs/2.2/mod/core.html#servername

Sometimes, the server runs behind a device that processes SSL, such as a reverse proxy, load balancer or SSL offload appliance. When this is the case, specify the https:// scheme and the port number to which the clients connect in the ServerName directive to make sure that the server generates the correct self-referential URLs.

Так же можно применить вот это решение Чиним ошибку 400 Bad Request с помощью mod_rpaf у BitrixEnv

Варианты подключения скрипта для Asset::getInstance()->addString() у 1С-Битрикс

, Михаил

Третий аргумент для функции addString() можно подсмотреть только в ядре: /bitrix/modules/main/lib/page/asset.php


<?php 

use Bitrix\Main\Page\Asset;
use Bitrix\Main\Page\AssetLocation;
Asset::getInstance()->addString("<script>***</script>"trueAssetLocation::AFTER_JS);
// AssetLocation::BEFORE_CSS;
// AssetLocation::AFTER_CSS;
// AssetLocation::AFTER_JS_KERNEL
// AssetLocation::AFTER_JS

?>

Подключение CSS и JS файлов в шаблоне компонента 1С-Битрикс

, Михаил

Для оформления и реализации front-end логики компонента, в шаблоне доступны не обязательные файлы

  • style.css — дополнительные стили для шаблона;
  • script.js — дополнительные скрипты для шаблона.

Архитектурно правильный способ - создать component_epilog.php


<?php 

global $APPLICATION;
$APPLICATION->AddHeadScript(SITE_TEMPLATE_PATH ."/js/divasoft.js");
$APPLICATION->SetAdditionalCss(SITE_TEMPLATE_PATH ."/css/divasoft.css");
$APPLICATION->AddHeadString("<link href='http://fonts.googleapis.com/css?family=PT+Sans:400&subset=cyrillic' rel='stylesheet' type='text/css'>");

?>

Или в новом стиле D7:


<?php 

use Bitrix\Main\Page\Asset;
Asset::getInstance()->addJs(SITE_TEMPLATE_PATH "/js/divasoft.js");
Asset::getInstance()->addCss(SITE_TEMPLATE_PATH "/css/divasoft.css");
Asset::getInstance()->addString("<link href='http://fonts.googleapis.com/css?family=PT+Sans:400&subset=cyrillic' rel='stylesheet' type='text/css'>");

?>

И теперь самый простой и правильный способ в template.php


<?php 

$this->addExternalCss("/local/styles.css");
$this->addExternalJS("/local/liba.js");

?>

Ещё один способ, если весь шаблон находится в отложенной функции template.php


<?php 

//we can't use $APPLICATION->SetAdditionalCSS()
$css $APPLICATION->GetCSSArray();
if(!is_array($css) || !in_array("/bitrix/css/main/font-awesome.css"$css)) {
	    $strReturn .= '<link href="'.CUtil::GetAdditionalFileURL("/bitrix/css/main/font-awesome.css").'" type="text/css" rel="stylesheet" />'."\n";
	}

?>

Подключение нескольких recaptcha2 в Битриксе

, Михаил

Есть несколько способов подключения и инициализации рекапч, но самый удобный это тот, который занимает мало строчек кода, и максимально эффективен.

В init.php добавляем код, инициализирующий рекапчу. Код добавляется структурно после подключения всех стилей и скриптов, что исключает ошибку раннего старта рекапчи. Код добавляется именно через addString(), а не через addScript():


<?php 

// Открытый ключ
define("DVS_RC_KEY","123");
// Секретный ключ 
define("DVS_RC_SECRET","321");
use Bitrix\Main\Page\Asset;
use Bitrix\Main\Page\AssetLocation;
if (!defined("DVS_RECAPTCHA")) {
	// Подключаем скрипт рекапчи
	    Asset::getInstance()->addString('<script src="//www.google.com/recaptcha/api.js?onload=divaCaptchaRender&render=explicit" async defer></script>',
	 trueAssetLocation::AFTER_JS);
	// Инициализируем массив рекапч
	    Asset::getInstance()->addString("<script>window.rc = {};
	 var divaCaptchaRender = function () {
		        $('.g-recaptcha').each(function() {
			          window.rc[$(this).attr('id')] = grecaptcha.render( this,{ 'sitekey': '" DVS_RC_KEY "', 'theme': 'light'} );
			});
		};
	</script>"trueAssetLocation::AFTER_JS);
	    define("DVS_RECAPTCHA"true);
	}

?>

Дальше мы можем обращаться из любого компонента к объекту рекапчи, которые находятся в массиве window.rc.

Вставка рекапчи:


<?php 
<div id="recaptchaUID" class="g-recaptcha"></div>
?>

Сброс рекапчи:


<?php 
<script>grecaptcha.reset(window.rc[recaptchaUID]);
</script>
?>

Тем самым не зная точно сколько рекапч будет на странице, мы можем работать с ними по идентификатору дива (в данном случае это recaptchaUID), в котором эта капча инициализировалась по классу g-recaptcha.

Принудительное включение https в ядре 1С-Битрикс

, Михаил

Есть недокументированная возможность заставить Битрикс работать по протоколу https (если не корректно настроен хостинг)

Создаём/редактируем файл bitrix/.settings_extra.php


<?php 

return array (
    "https_request" => array("value" => true),
);
 
?>

Как правильно включить отображение ошибок в 1С-Битрикс

, Михаил

В файле bitrix/.settings.php


<?php 
'exception_handling' => 
  array (
    'value' => 
    array (
      'debug' => true,
      'handled_errors_types' => E_ALL & ~E_NOTICE & ~E_STRICT & ~E_USER_NOTICE & ~E_DEPRECATED,
      'exception_errors_types' => E_ALL & ~E_NOTICE & ~E_WARNING & ~E_STRICT & ~E_USER_WARNING & ~E_USER_NOTICE & ~E_COMPILE_WARNING,
      'ignore_silence' => false,
      'assertion_throws_exception' => true,
      'assertion_error_type' => 256,
      'log' => 
      array (
        'settings' => 
        array (
          'file' => 'bitrix/err.log',
          'log_size' => 1000000,
        ),
      ),
    ),
    'readonly' => false,
  )
?>

Логи будут в файле bitrix/err.log

Как можно увеличить количество хранимых логов обмена с 1С на сайте 1С-Битрикс?

, Михаил

Для этого необходимо доработать компонент catalog.import.1c

Где в файле \bitrix\components\diva\catalog.import.1c\class.php

А именно в функции public function cleanUpDirectory($directoryName) есть условие


<?php 
if ($directoryEntry->isDirectory() && $directoryEntry->getName() === "Reports"{} 
?>

В котором нужно отключить $reportsEntry->delete();

Зачем лечить сайт от вирусов?

, Михаил

Вирусный код на страницах сайта может привести к полной блокировке доступа к сайту со стороны антивирусов или хостинга, а также бану в поисковых системах. Вирусы на сайте вредят посетителям, заражая их компьютеры вредоносным кодом и троянскими программами. Заполняют ваш сайт ссылками на другие сайты, что понижает ваши позиции в поисковых системах. Постоянно рассылают через ваш сайт вредоносные рекламные электронные письма.

Необходимо понимать, что вирусы являются следствием взлома, поэтому в первую очередь нужно найти источник появления вируса на сайте (скорее всего это будет взлом сайта через уязвимости в скриптах или кража пароля от FTP).

Если ваш сайт заразился, то Дивасофт поможем вам. Вылечим. Починим. Обновим.

Бесплатный SSL сертификат от Let's Encrypt

, Михаил

Получить сертификат на 90 дней можно у Let's Encrypt. Для удобства, пользуемся сервисами gethttpsforfree.com и  zerossl.com, что бы сгенерировать эти SSL сертификаты. Потом можно продлить сколько угодно раз. Как вручную, так и автоматически с помощью  certbot

90-дневные сертификаты — вовсе не новость для Всемирной паутины. Согласно телеметрии Firefox, 29% всех TLS-транзакций используют 90-дневные сертификаты, и ни одно иное время жизни не составляет большую долю транзакций. Точка зрения Let's Encrypt состоит в том, что короткие времена жизни сертификатов имеют два главных, основных преимущества:

  1. Ограничение ущерба от компрометированных ключей и неверно выпущенных сертификатов, так как таковые используются на меньшем промежутке времени.
  2. Короткоживущие сертификаты поддерживают и поощряют автоматизацию, которая абсолютно необходима для простоты использования HTTPS.

Меняем точность округления цены в 1С-Битрикс

, Михаил

Цена/Количество в 1С-Битрикс может округляться до 4 знака. Но что делать, если нам нужна точность до 10 знаков?

Устанавливаем константу /php_interface/init.php define("SALE_VALUE_PRECISION", 10);

В настройках модуля "Интернет-магазин" ставим
Знаков после запятой при выводе количественного значения: Авто

В свойствах таблицы b_sale_basket меняем все длины полей, что содержат 18,4 на 18,10

ALTER TABLE `b_sale_basket` MODIFY `PRICE` decimal(18,10) NOT NULL ;
ALTER TABLE `b_sale_basket` MODIFY `BASE_PRICE` decimal(18,10) NULL DEFAULT NULL  ;
ALTER TABLE `b_sale_basket` MODIFY `QUANTITY` decimal(18,10) NOT NULL DEFAULT "0.0000000000000" ;

Если заказ создаётся через API, а не через админку - то всё хорошо. Если через админку будем менять цену за единицу товара - то всё хорошо, но если "сохранить" без изменений... точность сбросится до 4го знака.

Добавляем ID профиля покупателя в свойство заказа 1С-Битрикс на ядре D7

, Михаил
  1. Создаём поле для хранения идентификатора покупателя SUBUSER_ID
  2. Добавляем обработчик OnBeforeSaleOrderFinalAction, что бы установить наше свойство.

<?php 
\Bitrix\Main\EventManager::getInstance()->addEventHandler('sale''OnBeforeSaleOrderFinalAction''OnBeforeSaleOrderFinalActionHandler');
function OnBeforeSaleOrderFinalActionHandler(\Bitrix\Main\Event $event) {
	    $order $event->getParameter('ENTITY');
	    $userProfileId getContragentID($order->getId());
	    $propertyCollection $order->getpropertyCollection();
	    foreach ($propertyCollection as $property) {
		        if($property->getField('CODE') == 'SUBUSER_ID') {
			            $property->setValue($userProfileId);
			}
		}
	    return new \Bitrix\Main\EventResult(
	        \Bitrix\Main\EventResult::SUCCESS, array(
	            "ENTITY" => $order,
	        )
	    );
	}
?>

  1. Дальше находим ID профиля покупателя по ИНН у Юр.Лиц, и по ФИО(названию профиля) у Физ.Лиц.

<?php 
function getContragentID($orderID) {
	    CModule::IncludeModule("sale");
	    if ($arOrder CSaleOrder::GetByID($orderID)) {
		        $profiles = \Bitrix\Sale\Helpers\Admin\Blocks\OrderBuyer::getBuyerProfilesList($arOrder['USER_ID'],$arOrder['PERSON_TYPE_ID']);
		        unset($profiles[0]);
		        $profiles array_flip($profiles);
		        $filterValue = array('PERSON_TYPE_ID' => $arOrder['PERSON_TYPE_ID'], 'IS_PROFILE_NAME' => 'Y');
		        if ($arOrder['PERSON_TYPE_ID']==2) {
			            $filterValue = array('PERSON_TYPE_ID' => $arOrder['PERSON_TYPE_ID'], 'CODE' => 'INN');
			        }
		        $rsOrderProps CSaleOrderProps::GetList(array(), $filterValue);
		        if ($arOrderProp $rsOrderProps->Fetch()) {
			            $rsProps CSaleOrderPropsValue::GetList(array('SORT' => 'ASC'), array('ORDER_ID' => $orderID'ORDER_PROPS_ID' => $arOrderProp['ID']));
			            if ($arProp $rsProps->Fetch()) {
				                $rsUP CSaleOrderUserPropsValue::GetList(array(), array('ORDER_PROPS_ID' => $arOrderProp['ID'],
				                    'VALUE' => $arProp['VALUE'],
				                    'PROP_PERSON_TYPE_ID' => $arOrder['PERSON_TYPE_ID']));
				                if ($arUP $rsUP->Fetch()) {
					                    if (array_key_exists($arProp['VALUE'], $profiles) && $arOrder['PERSON_TYPE_ID']==1) {
						                        return $profiles[$arProp['VALUE']];
						                    }
					                    return $arUP['USER_PROPS_ID'];
					                } else {
					                    $db_sales CSaleOrderUserProps::GetList(array(),array("USER_ID" => $arOrder['USER_ID'],"PERSON_TYPE_ID"=>$arOrder['PERSON_TYPE_ID'],"=NAME"=>$arProp['VALUE']));
					                    if ($ar_sales $db_sales->Fetch()) {
						                       return $ar_sales["ID"];
						                    }
					                }
				            } 
			        }
		    }
	}
?>

Что такое FUSER_ID в 1С-Битриксе

, Михаил

ID покупателя (так называемый FUSER_ID) уникальный для посетителя, открывшего сайт. Он у него живет в куках, даже если он еще не делал ничего. Сейчас поясню, для чего он нужен. В продукте реализован механизм, который позволяет работать с корзиной не авторизованным пользователям. Чтобы этот механизм работал, корзина привязывается не к ID пользователя сайта, а к FUSER_ID - идентификатор пользователя магазина, который записывается в куки. Опишу несколько ситуаций:

Ситуация первая:

  1. Вы не авторизованный пользователь, кладёте товар в корзину - создаётся новый FUSER_ID, пусть это FUSER_X, который записывается в базу и в куки.
  2. Затем вы авторизуетесь под аккаунтом пользователя A. Если к идентификатору пользователя А не привязан никакой FUSER_ID, то он опять же создаётся, привязывается к идентификатору пользователя А (записывается в базу) и пишется в куки, путь это будет FUSER_Y. В это же время все товары не авторизованного пользователя переносятся в корзину авторизованного пользователя.
    Товары покупателя FUSER_X ---> Товары покупателя FUSER_Y.
    Соответственно, теперь к FUSER_X не будет привязано ни одного товара и FUSER_X удаляется из базы.

Ситуация вторая:

  1. Вы авторизованный пользователь А, вашей корзине есть товары, которые привязаны к FUSER_Y.
  2. Вы решили разлогиниться.
  3. В этот момент Вы становитесь не авторизованным пользователем и FUSER_ID из куков удаляется и пишется НОВЫЙ, корзина пуста, т.к. именно по FUSER_ID осуществляется выборка товаров в корзине.

Ситуация третья:

  1. Вы авторизованный пользователь А, вашей корзине есть товары, которые привязаны к FUSER_Y.
  2. Вы не собираетесь разлогиниваться, но получается так, что закончилось время жизни сессии - вы становитесь не авторизованным пользователем. Из куков не удаляется FUSER_ID, он остаётся таким же равным FUSER_Y, т.е. получается, что вы не авторизованный пользователь с FUSER_ID пользователя А, поэтому в корзине присутствуют товары пользователя А.
    Это нормальная ситуация. Согласитесь, пользователю будет не очень приятно, если он набрал 50 товаров в корзину, время сессии закончилось и его товары из корзины пропали.
  3. За этот же компьютер садится другой пользователь, который авторизуется под аккаунтом пользователя В, идентификатор пользователя магазина FUSER_Z. И тут возникает "первая ситуация", перенос товаров от FUSER_Y к FUSER_Z.
  4. В итоге, если пользователь А, авторизуется снова (на другом компьютере или браузере), то его корзина будет пуста.
    Т.е. пользователь B "приобрёл" корзину пользователя А.

Если пользователь А и пользователь B, работают за разными компьютерами, то ситуации переноса корзины от пользователя А к пользователю B никогда не произойдёт, соответственно проблему у покупателей быть не должно.
Это неизбежная логика стандартного механизма корзины.

p.s. FUSER_ID это ID покупателя, но НЕ ID профиля покупателя, это разные сущности.

Возвращаем блок "Предложите покупателю" в детальный заказ 1С-Битрикс

, Михаил
Добавляем в /bitrix/php_interface/admin_header.php

<?php 
<? if (stripos($_SERVER['REQUEST_URI'], '/bitrix/admin/sale_order_view.php') !== false) : ?>
    <script type="text/javascript">
        $(function () {
	            $.ajax({  method: "GET", url: "/bitrix/diva/ajaxGetBasket.php", data: {ID: <?= intval($_REQUEST['ID']) ?>}
		}).done(function (msg) {
		                $('.adm-s-result-container').prepend(msg);
		});
	});
    </script>
<?endif;
?>
?>

Создаём обработчик /bitrix/diva/ajaxGetBasket.php

<?php 

define("NO_KEEP_STATISTIC"true);
//define("NOT_CHECK_PERMISSIONS", true);
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");
$ID intval($_REQUEST['ID']);
if (!$USER->IsAdmin() || $ID == 0) {
	    die();
	}
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/sale/general/admin_tool.php");
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/sale/lang/ru/admin/order_detail.php");
CModule::IncludeModule("iblock");
CModule::IncludeModule("catalog");
CModule::IncludeModule("sale");
$dbOrder CSaleOrder::GetList(
                array("ID" => "DESC"), array("ID" => $ID), falsefalse, array(
            "ID""LID""PERSON_TYPE_ID",
            "PAYED""DATE_PAYED""EMP_PAYED_ID""PAY_VOUCHER_NUM""PAY_VOUCHER_DATE",
            "CANCELED""DATE_CANCELED""EMP_CANCELED_ID""REASON_CANCELED",
            "STATUS_ID""DATE_STATUS""EMP_STATUS_ID""PRICE_DELIVERY",
            "ALLOW_DELIVERY""DATE_ALLOW_DELIVERY""EMP_ALLOW_DELIVERY_ID",
            "DEDUCTED""DATE_DEDUCTED""EMP_DEDUCTED_ID""REASON_UNDO_DEDUCTED",
            "MARKED""DATE_MARKED""EMP_MARKED_ID""REASON_MARKED",
            "PRICE""CURRENCY""DISCOUNT_VALUE""SUM_PAID""USER_ID""PAY_SYSTEM_ID",
            "DELIVERY_ID""DATE_INSERT""DATE_INSERT_FORMAT""DATE_UPDATE""USER_DESCRIPTION",
            "ADDITIONAL_INFO""PS_STATUS""PS_STATUS_CODE""PS_STATUS_DESCRIPTION",
            "PS_STATUS_MESSAGE""PS_SUM""PS_CURRENCY""PS_RESPONSE_DATE""COMMENTS",
            "TAX_VALUE""STAT_GID""RECURRING_ID""AFFILIATE_ID""LOCK_STATUS",
            "USER_LOGIN""USER_NAME""USER_LAST_NAME""USER_EMAIL""DELIVERY_DOC_NUM",
            "DELIVERY_DOC_DATE""STORE_ID""ACCOUNT_NUMBER""TRACKING_NUMBER",
                )
);
if (($arOrder $dbOrder->Fetch())) {
	    ?>
	    <div class="load_product order_summary" style="float: left;
	">
	        <table width="100%" class="itog_header"><tr><td>Предложите покупателю</td></tr></table>
	        <div id="tabs">
	            <?
	            $crmMode false;
	            $displayNone "block";
	            $displayNoneBasket "block";
	            $displayNoneViewed "block";
	            $arFilterRecomendet = array();
	            $arBasketItems = array();
	            $dbBasketTmp CSaleBasket::GetList(array("ID" => "ASC"), array("ORDER_ID" => $arOrder["ID"]), falsefalse, array("ID""PRODUCT_ID"));
	            while ($arBasketTmp $dbBasketTmp->GetNext()) {
		                $arBasketItems[] = $arBasketTmp;
		            }
	            //pr($arBasketItems);
	            foreach ($arBasketItems as $arItem) {
		                if (!CSaleBasketHelper::isSetItem($arItem)) {
			                    $arFilterRecomendet[] = $arItem["PRODUCT_ID"];
			                }
		            }
	            $arRecommendedResult CSaleProduct::GetRecommendetProduct($arOrder["USER_ID"], $arOrder["LID"], $arFilterRecomendet);
	            $recomCnt count($arRecommendedResult);
	            if ($recomCnt 2) {
		                $arTmp = array();
		                $arTmp[] = $arRecommendedResult[0];
		                $arTmp[] = $arRecommendedResult[1];
		                $arRecommendedResult $arTmp;
		            }
	            if ($recomCnt <= 0)
	                $displayNone "none";
	            $arErrors = array();
	            $arFuserItems CSaleUser::GetList(array("USER_ID" => intval($arOrder["USER_ID"])));
	            $arCartWithoutSetItems = array();
	            $arTmpShoppingCart CSaleBasket::DoGetUserShoppingCart($arOrder["LID"], $arOrder["USER_ID"], $arFuserItems["ID"], $arErrors, array());
	            if (is_array($arTmpShoppingCart)) {
		                foreach ($arTmpShoppingCart as $arCartItem) {
			                    if (CSaleBasketHelper::isSetItem($arCartItem))
			                        continue;
			                    $item findPositionsByID($arCartItem["PRODUCT_ID"]);
			                    if ($item['IBLOCK_ID'] != CATALOG_IBLOCK_ID) {
				                        if ($item['PROPS']['CML2_LINK']['VALUE'] != "") {
					                            $item findPositionsByID($item['PROPS']['CML2_LINK']['VALUE']);
					                        }
				                    }
			                    $arCartItem['MASTER'] = $item;
			                    $arCartItem['NAME'] = "[" $item['PROPS']['CML2_ARTICLE']['VALUE'] . "] " $arCartItem['NAME'];
			                    $arCartWithoutSetItems[] = $arCartItem;
			                }
		            }
	            $basketCnt count($arCartWithoutSetItems);
	            if ($basketCnt 2) {
		                $arTmp = array();
		                $arTmp[] = $arCartWithoutSetItems[0];
		                $arTmp[] = $arCartWithoutSetItems[1];
		                $arCartWithoutSetItems $arTmp;
		            }
	            if ($basketCnt <= 0)
	                $displayNoneBasket "none";
	            ///
	            $arViewed = array();
	            $arViewedIds = array();
	            $viewedCount 0;
	            $mapViewed = array();
	            if (CModule::includeModule("catalog")) {
		                $viewedIterator = \Bitrix\Catalog\CatalogViewedProductTable::getList(array(
		                            'order' => array("DATE_VISIT" => "DESC"),
		                            'filter' => array('FUSER_ID' => $arFuserItems["ID"], "SITE_ID" => $arOrder["LID"]),
		                            'select' => array("ID""FUSER_ID""DATE_VISIT""PRODUCT_ID""LID" => "SITE_ID""NAME" => "ELEMENT.NAME""PREVIEW_PICTURE" => "ELEMENT.PREVIEW_PICTURE""DETAIL_PICTURE" => "ELEMENT.DETAIL_PICTURE")
		                ));
		                while ($viewed $viewedIterator->fetch()) {
			                    $viewed['MODULE'] = 'catalog';
			                    $arViewed[$viewedCount] = $viewed;
			                    $arViewedIds[] = $viewed['PRODUCT_ID'];
			                    $mapViewed[$viewed['PRODUCT_ID']] = $viewedCount;
			                    $viewedCount++;
			                }
		                unset($viewedCount);
		                $baseGroup CCatalogGroup::getBaseGroup();
		                if (!empty($arViewedIds)) {
			                    $priceIterator CPrice::getList(
			                                    array(), array("PRODUCT_ID" => $arViewedIds'CATALOG_GROUP_ID' => $baseGroup['ID']), falsefalse, array("PRODUCT_ID""PRICE""CURRENCY"));
			                    while ($productPrice $priceIterator->fetch()) {
				                        if (isset($mapViewed[$productPrice['PRODUCT_ID']])) {
					                            $key $mapViewed[$productPrice['PRODUCT_ID']];
					                            $arViewed[$key]["PRICE"] = $productPrice["PRICE"];
					                            $arViewed[$key]["CURRENCY"] = $productPrice["CURRENCY"];
					                        }
				                    }
			                }
		                $viewedCnt count($arViewed);
		                $arViewed array_slice($arViewed02);
		                if (count($arViewed) <= 0)
		                    $displayNoneViewed "none";
		            }
	            else {
		                $displayNoneViewed "none";
		            }
	            $tabBasket "tabs";
	            $tabViewed "tabs";
	            if ($displayNoneBasket == 'none' && $displayNone == 'none' && $displayNoneViewed == 'block')
	                $tabViewed .= " active";
	            if ($displayNoneBasket == 'block' && $displayNone == 'none')
	                $tabBasket .= " active";
	            ?>
	            <div id="tab_1" style="display:<?= $displayNone ?>"       class="tabs active"     onClick="fTabsSelect('buyer_recmon', this);
	" ><?= GetMessage('SOD_SUBTAB_RECOMENET'?></div>
	            <div id="tab_2" style="display:<?= $displayNoneBasket ?>" class="<?= $tabBasket ?>" onClick="fTabsSelect('buyer_basket', this);
	"><?= GetMessage('SOD_SUBTAB_BASKET'?></div>
	            <div id="tab_3" style="display:<?= $displayNoneViewed ?>" class="<?= $tabViewed ?>" onClick="fTabsSelect('buyer_viewed', this);
	"><?= GetMessage('SOD_SUBTAB_LOOKED'?></div>
	            <?
	            if ($displayNone == 'block') {
		                $displayNoneBasket 'none';
		                $displayNoneViewed 'none';
		            }
	            if ($displayNoneBasket == 'block') {
		                $displayNone 'none';
		                $displayNoneViewed 'none';
		            }
	            if ($displayNoneViewed == 'block') {
		                $displayNone 'none';
		                $displayNoneBasket 'none';
		            }
	            ?>
	            <div id="buyer_recmon" class="tabstext active" style="display:<?= $displayNone ?>">
	                <? echo fGetFormatedProductData($arOrder["USER_ID"], $arOrder["LID"], $arRecommendedResult$recomCnt$arOrder["CURRENCY"], 'recom'$crmMode);
	 ?>
	            </div>
	            <div id="buyer_basket" class="tabstext active" style="display:<?= $displayNoneBasket ?>">
	                if (count($arCartWithoutSetItems) > 0)
	                echo fGetFormatedProductData($arOrder["USER_ID"], $arOrder["LID"], $arCartWithoutSetItems, $basketCnt, $arOrder["CURRENCY"], 'basket', $crmMode);
	                ?>
	            </div>
	            <div id="buyer_viewed" class="tabstext active" style="display:<?= $displayNoneViewed ?>">
	                <?
	                if (count($arViewed) > 0)
	                    echo fGetFormatedProductData($arOrder["USER_ID"], $arOrder["LID"], $arViewed$viewedCnt$arOrder["CURRENCY"], 'viewed'$crmMode);
	                ?>
	            </div>
	        </div>
	        <script type="text/javascript">
	            function fTabsSelect(tabText, el)
	            {
		                BX('tab_1').className = "tabs";
		                BX('tab_2').className = "tabs";
		                BX('tab_3').className = "tabs";
		                BX(el).className = "tabs active";
		                BX(el).className = "tabs active";
		                BX(el).style.display = 'block';
		                BX('buyer_recmon').className = "tabstext";
		                BX('buyer_basket').className = "tabstext";
		                BX('buyer_viewed').className = "tabstext";
		                BX('buyer_recmon').style.display = 'none';
		                BX('buyer_basket').style.display = 'none';
		                BX('buyer_viewed').style.display = 'none';
		                BX(tabText).style.display = 'block';
		                BX(tabText).className = "tabstext active";
		            }
	        </script>
	        <script type="text/javascript">
	            /*
	             * click on recommendet More
	             */
	            function fGetMoreProduct(type)
	            {
		                BX.showWait();
		                productData = <? echo CUtil::PhpToJSObject($arFilterRecomendet);
		 ?>;
		                var userId = '<?= $arOrder["USER_ID"?>';
		                var fUserId = '<?= $arFuserItems["ID"?>';
		                var currency = '<?= $arOrder["CURRENCY"?>';
		                var lid = '<?= $arOrder["LID"?>';
		                BX.ajax.post('/bitrix/admin/sale_order_detail.php', '<?= CUtil::JSEscape(bitrix_sessid_get()) ?>&ORDER_AJAX=Y&type=' + type + '&arProduct=' + productData + '&currency=' + currency + '&LID=' + lid + '&userId=' + userId + '&fUserId=' + fUserId + '&ID=<?= $ID ?>', fGetMoreProductResult);
		            }
	            function fGetMoreProductResult(res)
	            {
		                BX.closeWait();
		                var rs = eval('(' + res + ')');
		                if (rs["ITEMS"].length > 0)
		                {
			                    if (rs["TYPE"] == 'basket')
			                        BX("buyer_basket").innerHTML = rs["ITEMS"];
			                    if (rs["TYPE"] == 'recom')
			                        BX("buyer_recmon").innerHTML = rs["ITEMS"];
			                    if (rs["TYPE"] == 'viewed')
			                        BX("buyer_viewed").innerHTML = rs["ITEMS"];
			                }
		            }
	        </script>    
	    </div>
	    <?
	}
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/epilog_after.php");

?>

SECTION_CODE_PATH не учитывает отсутствие символьного кода родительского раздела (1С-Битрикс)

, Михаил

<?php 
У catalog.section->result_modifier.php
foreach ($arResult["ITEMS"] as &$arItem) {
	   // FIX: SECTION_CODE_PATH
	   $nav CIBlockSection::GetNavChain($arItem['IBLOCK_ID'], $arItem['~IBLOCK_SECTION_ID']);
	   $arNavi=array();
	   while($arNavItem $nav->Fetch()) {
		        $arNavi[]=$arNavItem['CODE'];
		}
	   $arNavi[]=$arItem['CODE'];
	   $arItem['DETAIL_PAGE_URL']="/".implode("/"$arNavi)."/";
	}

?>

Подключаем почтовый сервис для рассылок электронных писем mailgun.com для 1С-Битрикс

, Михаил

Скачиваем библиотеку https://github.com/mailgun/mailgun-php


<?php 
require_once $_SERVER['DOCUMENT_ROOT'].'/bitrix/divasoft/mailgun-php/vendor/autoload.php';
use Mailgun\Mailgun;
function custom_mail($to$subject$message$additionalHeaders ''){
	        $mg = new Mailgun("key-*****");
	        $domain "divasoft.ru";
	/*$mg = new Mailgun('key-*****', null, 'bin.mailgun.net');
	 // TEST
	$mg->setApiVersion('*******');
	*/
	      //Получаем тему письма
	    $elements imap_mime_header_decode($subject);
	    $title =  '';
	    for ($i=0;
	 $i<count($elements);
	 $i++) {
		        $title .= $elements[$i]->text;
		    }
	        preg_match('/From: (.+)\n/i'$additionalHeaders$matches);
	        list(, $from) = $matches;
	        preg_match('/BCC: (.+)\n/i'$additionalHeaders$matches);
	        list(, $bcc) = $matches;
	        preg_match('/CC: (.+)\n/i'$additionalHeaders$matches);
	        list(, $cc) = $matches;
	        preg_match('/Reply-To: (.+)\n/i'$additionalHeaders$matches);
	        list(, $rt) = $matches;
	        $mg->sendMessage($domain, array('from'    => $from, 
	                                        'to'      => $to, 
	                                        'bcc'      => $bcc, 
	                                        'subject' => $subject, 
	                                        'html' => $message, 
	                                        'text'    => strip_tags($message)));
	        // DEBUG
	        /*$result = $mg->get("$domain/log", array('limit' => 25, 
	                                                'skip'  => 0));
	        $logItems = $result->http_response_body->items;
	        foreach($logItems as $logItem){
		            //echo $logItem->message_id . "\n";
		}*/
	        return true;
	}
?>

И волшебный ответ для того, что бы аккаунт прошёл валидацию
"your account is temporarily disabled. business verification please contact support to resolve"

Hello!

What types of emails will you be sending - transactional or marketing?
transactional + marketing
Register info, Order info, Promo mail.

Where do you source your database of email addresses?
CMS 1C-Bitrix (divasoft.ru)

Are all of your email addresses double-opt in?
I do not quite understand, but the mail has.

What is your expected monthly volume of messages?
100-1000, we just start online-store

Have you read our Email Best Practices document? 
Yes!

Can you please give us the URL that your users use to sign up for your email as well as a link to your Terms of Service?
In development, link to register page = https://divasoft.ru/login/?register=yes&backurl=%2F

p.s. этот сервис скрывает ip адрес сервера отправителя

Тонкий пробел в цене для 1С-Битрикс

, Михаил

Если у вас php 5.3, то понадобится обработчик


<?php 
/bitrix/php_interface/init.php
// Установка тонкого пробела в цене для php 5.3
AddEventHandler("currency""CurrencyFormat""thinNbspFormat");
function thinNbspFormat($fSum$strCurrency) {
	    $separator '&#8201;
	';
	    $summ preg_replace('/(?<=\d)\x' bin2hex($separator[0]) . '(?=\d)/'$separatornumber_format($fSum2'.'$separator)).$separator.'руб.';
	    $summ str_replace(".00"""$summ);
	    return $summ
	}
?>

Дальше в Админ-панели, в настройках Валюты->Языковые настройки (/bitrix/admin/currency_edit.php?lang=ru&ID=RUB)

Строка формата для вывода валюты: #&thinsp;руб.

Разделитель тысяч при выводе: Другое значение &thinsp;

Дальше меняем длину одного столбца в SQL таблице: ALTER TABLE `b_catalog_currency_lang` CHANGE `THOUSANDS_SEP` `THOUSANDS_SEP` varchar(10) COLLATE 'utf8_unicode_ci' NULL DEFAULT ' ' AFTER `DEC_POINT`;

И устанавливаем нужное значение разделителя вручную (т.к. в админке ограничение на 5 символов при вводе): UPDATE `b_catalog_currency_lang` SET `THOUSANDS_SEP` = ' ' WHERE `CURRENCY` = 'RUB' AND `LID` = 'ru';

Разные цены для разных сайтов 1С-Битрикс

, Михаил

В /bitrix/php_interface/init.php


<?php 
global $TYPE_PRICE;
$TYPE_PRICE 3;
 //ID цены на 1м сайте
?>

В /bitrix/php_interface/s2/init.php добавляем обработчик:


<?php 
AddEventHandler("catalog""OnGetOptimalPrice"'OnGetOptimalPriceHandler');
global $TYPE_PRICE;
$TYPE_PRICE 4;
  //ID цены на 2м сайте
function OnGetOptimalPriceHandler($productID$quantity 1$arUserGroups = array(), $renewal "N"$arPrices = array(), $siteID "s2"$arDiscountCoupons false) {
	    CModule::IncludeModule("iblock");
	    Cmodule::IncludeModule('catalog');
	    global $TYPE_PRICE;
	    $db_res CPrice::GetList(array(), array("PRODUCT_ID" => $productID"CATALOG_GROUP_ID" => $TYPE_PRICE));
	    if ($ar_res $db_res->Fetch()) {
		        $price $ar_res['PRICE'];
		        $currency $ar_res['CURRENCY'];
		        $arResult = array(
		            'PRICE' => array(
		                'PRICE' => $price,
		                'CURRENCY' => $currency,
		            )
		        );
		        $arDiscounts CCatalogDiscount::GetDiscount($productID15);
		 // ID Инфоблока с торговыми предложениями (в данном случае)
		        if ($arDiscounts) {
			            foreach ($arDiscounts as $arDiscount) {
				                $arResult['DISCOUNT_LIST'][] = array(
				                    'VALUE_TYPE' => $arDiscount['VALUE_TYPE'],
				                    'VALUE' => $arDiscount['VALUE'],
				                    'CURRENCY' => $arDiscount['CURRENCY']
				                );
				}
			}
		} else {
		        return true;
		}
	    return $arResult;
	}
?>

Ставим статус "Отменен" в заказе 1С-Битрикс

, Михаил

Ставим статус "Отменен" в заказе 1С-Битрикс

Для этого правим:


<?php 
/bitrix/modules/sale/general/order_loader.php
function collectOrderInfo($value) {
	***
	$arOrder["TRAITS"] = array();
	$arOrder["TRAITS"][GetMessage("CC_BSC1_CANCELED")] = $value["#"][GetMessage("CC_BSC1_CANCELED")][0]["#"];
	}

?>

Добавляем SITE_ID в экспорт/импорт заказов 1С-Битрикс

, Михаил

Нам нужно отправлять/принимать/обновлять id сайта, на котором был сделан заказ.

Для этого правим:

Добавляем SITE_ID в выгрузку заказа


<?php 

/bitrix/modules/sale/general/export.php
<<?=GetMessage("SALE_EXPORT_DOCUMENT")?>>
<SITE_ID><?=$arOrder["LID"]?></SITE_ID>

?>

Добавляем SITE_ID в импорт заказа, сохраняя основную функциональность


<?php 
/bitrix/modules/sale/general/order_loader.php
function collectOrderInfo($value) {
	***
	$arOrder["ID"] = $value["#"][GetMessage("CC_BSC1_NUMBER")][0]["#"];
	$arOrder["SITE_ID"] = ($value["#"]["SITE_ID"][0]["#"])?$value["#"]["SITE_ID"][0]["#"]:$this->arParams["SITE_NEW_ORDERS"];
	}

?>


<?php 
/bitrix/modules/sale/general/order_loader.php
if ($orderInfo['SITE_ID']!=$arOrder['SITE_ID']) {
	    $arAditFields["SITE_ID"]=$arOrder['SITE_ID'];
	    $arAditFields["UPDATED_1C"] = "Y";
	}
****
if(count($arAditFields)>0)
    CSaleOrder::Update($orderInfo["ID"], $arAditFields);

?>


<?php 
/bitrix/modules/sale/general/order_loader.php
if(IntVal($arOrder["USER_ID"]) > 0) {
	$orderFields = array( "SITE_ID" => $arOrder["SITE_ID"],
	***
	)
	}

?>

Если у вас DDoS или почему не стоит пользоваться услугами Rusonyx

, Ashe Gentle

Солнечное утро пятницы не предвещало ничего плохого, когда раздался звонок от клиента: «Долго открывается сайт». «Слишком много пользователей», — первым делом решили мы, потому что такое уже случалось. Да и хостер не отреагировал на резкое увеличение трафика, так что причин для беспокойства пока ещё не было. Полезли в админку, чтобы в очередной раз прибавить мощностей, но от представших нашему взору графиков потеряли дар речи.

Графики нагрузки

Ответ мог быть только один — DDoS. И тут неожиданно очнулся Rusonyx и отключил к чёрту наш сайт. Не, ну а что? Конечно, мы написали в техподдержку и спустя полтора часа получили пространный ответ:

На Ваш сервер зафиксирован сильный поток входящего трафика, IP адрес заблокирован. Вам нужно: 1. Обратиться к компании, предоставляющей услуги фильтрации траффика (Qurator например). 2. Купить новый IP адрес. 3. Сообщить нам информацию, которую Вам предоставили (Qurator например), далее мы сделаем все самостоятельно.

Сказано, сделано. Покупаем IP-адрес всё у того же Rusonyx. Благоразумно отказываемся от предложенного Qurator, ибо есть сервисы куда дешевле и надёжнее, к ним и обращаемся. И всю полученную информацию скидываем обратно хостеру. Ждём. И, внезапно, обнаруживаем, что свежекупленный IP тоже попал под DDoS. «КАК?!» — спрашиваем у хостера, — «...» Покупаем ещё один IP и снова ждём. Ждём. Ждём. Тут стоит упомянуть, что у русоникса две линии техподдержки: круглосуточная для простых обращений и инженерная (не работающая по выходным, да) для вот таких проблем. А меж тем пятница стремительно проходит.

Изрядно переживаем, потому что клиент теряет прибыль, а мы — нервные клетки. Мандража добавляет отсутствие доступа к панели управления и невозможность снять свежие бекапы сайта. Снова и снова бомбардируем техподдержку хостера, как наступает он — вечер пятницы, злоклятое время для всех админов. На этом, видимо, рабочий день инженеров русоникса заканчивается и они со спокойной совестью уходят домой. Напоминаю, вся информация для защиты у них есть уже несколько часов, а сайт всё ещё DDoS'ят. С сожалением понимаем, что не можем ничего сделать до понедельника. В нервном ожидании проводим и выходные и, наконец, получаем в воскресенье наши бэкапы. И первое, что делаем — уходим к другому хостеру.

Итого

  1. Мы скрыли сайт системой фильтрации трафика на уровне DNS.
  2. Отказались от услуг недобросовестного, а точнее бессовестного русоникса.
  3. Защитили исходящие сообщения от почтового сервиса сайта через сторонний SMTP-сервис.
  4. Вышли из-под DDoS.
  5. Дополнительно узнали, что у клиента есть серьёзно настроенные конкуренты.
  6. ...
  7. Profit!

Ах да, чуть не забыла самое интересное, вместе с недоступностью сайта клиента просто катастрофически выросли посещения на наш сайт ;)

Посещаемость сайта divasoft.ru

P.S.: Обратили внимание, что в посте не названы сервисы и ресурсы, которые мы использовали? Конечно это сделано нарочно, для обеспечения безопасности. Впрочем, все они легко гуглятся, если вас интересует эта тема.

Лень — двигатель прогресса

, Ashe Gentle

Знаете, что случается, когда человеку становится лень? Правильно. Он придумывает способ (иногда новаторский и гениальный) облегчить свою работу. Поэтому у нас сразу две хорошие новости.

Во-первых, я решила в своём напряжённом и невероятно плотном графике выделить место для регулярных публикаций. А это значит, что блог Дивасофт снова будет вестись и пополняться интересными материалами. Но, увы, времени на оповещение об этом соцсетей не нашлось.

Поэтому, во-вторых, мы стали использовать удобный, но пока несовершенный (не хватает нам выгрузки в гугл+, но её уже делают) модуль CrossTopus. Помимо прочих плюшек, у ребят отличная техподдержка, с которой мы легко нашли общий язык и даже подружились. Но это уже совсем другая история.

Ах, да. Ещё у нас обновлены иконки соцсетей, как там говорят в таких случаях? Подписывайтесь, ставьте лайки, ждите свежих новостей =)

Открытие блога

, Ashe Gentle

Как и всякая хорошая вещь, блог «Дивасофт» начинался с идеи. Чужой. Где-то на просторах интернета мы подсмотрели дизайн с широкими картинками и узким текстом. Отталкиваясь от него, приступили к созданию своего варианта. Долго, очень долго мы вымеряли шаблоны. Придумывали, где разметить дату и подпись, использовать ли теги, как обойтись с картинками, что делать с разделением по страницам. И вот, спустя чуть ли не полгода, блог «Дивасофт» запущен и работает.

И он красивый. Да, вот так вот нескромно с нашей стороны это заявлять, но посудите сами. Чего только стоит исходный код страницы, залюбоваться можно. Разметка всех и вся сделана по стандартам HTML5. Вы только посмотрите на структуру каждого поста!

<article>
    <header>
        <h2><a href="">Открытие блога</a></h2>
        <span><time datetime="2014-05-16T12:00:00+04:00">16 Мая 2014</time>, <em>Ashe Gentle</em></span>
    </header>
    <p>Как и всякая хорошая вещь, блог «Дивасофт» начинался с идеи. Чужой. Где-то на просторах интернета мы подсмотрели дизайн с широкими картинками и узким текстом. Отталкиваясь от него, приступили к созданию своего варианта. Долго, очень долго мы вымеряли шаблоны. Придумывали, где разметить дату и подпись, использовать ли теги, как обойтись с картинками, что делать с разделением по страницам. И вот, спустя чуть ли не полгода, блог «Дивасофт» запущен и работает.</p>
    <h3>Подзаголовок</h3>
    <p>И он красивый. Да, вот так вот нескромно с нашей стороны это заявлять, но посудите сами. Чего только стоит исходный код страницы, залюбоваться можно. Разметка всех и вся сделана по стандартам HTML5. Вы только посмотрите на структуру каждого поста!</p>
    <footer>
        <em><a href="#">Бекэнд</a>, <a href="#">Bitrix</a></em>
    </footer>
</article>

Тут тебе и <article>, и <header>, и <footer>, и <time> и всё-всё-всё на свете. А как замечательно оформлены у нас цитаты:

<blockquote cite="http://divasoft.ru/blog/">
    <p>А как замечательно оформлены у нас цитаты</p>
</blockquote>

Отдельно стоит упомянуть типографику. Вы обратили внимание, что мы используем верные кавычки, принятые в русском языке — «ёлочки»? И, конечно, длинные тире вместо дефисов и минусов, которые, между прочим, не так просто поставить. Приходится использовать либо цифровые коды, либо специальные символы html.

А ещё мы точно продумали расположение изображений. Их может быть четыре рядом, три, два или одно на весь блок записи. И при всём этом отступы между ними будут равными. Пришлось повозиться с полпикселями, появляющимися в разных браузерах. Но и эту проблему мы решили, использовав float: right; для крайнего правого изображения.

Кстати, о полпикселях. Это происходит потому, что мы используем собственную процентную сетку. Которая тоже была разработана путём долгих и кровавых споров. У нас вообще никогда без них не обходится. Сейчас, например, воюем за или против переносов слов.

Всё вышесказанное относилось к вёрстке, но и с натягом вышло не так просто. Изначально планировалось использовать стандартный компонент битрикс — блог. Но когда уже всё было готово и мы приступили к тестированию, возникли непредвиденные проблемы. Оказалось, что начиная с 12 версии, разработчики битрикса запретили использование HTML-редактора в блогах. Наверное, это связано с безопасностью. И наверное, важно. Но нам абсолютно не подходит. Так реализация проекта застопорилась на неопределённое время, пока мы искали пути обхода. Как вариант рассматривали использование другой площадки — wordpress, например. Заточенный именно для блогов, он мог бы стать решением всех проблем. Но оказалось нецелесообразным разбивать единую систему. Поэтому мы сделали блог на инфоблоках.

И увидел Бог всё, что Он создал, и вот, хорошо весьма. И был вечер, и было утро...

Возможно, потом у нас будут комментарии и подписка, и конечно «новое видение» основного сайта. Пока это всё в проекте и в необозримом будущем. Но уже сейчас вы можете читать о жизни компании, её проблемах, решениях, разработках, проектах и о многом другом. Присоединяйтесь!