Разработка ПО

«Лента друзей»: разрабатываем компонент «1С-Битрикс»

Сергей Лещенко (sle_e@mail.ru)

Прошло время, когда социальные сети считались бесполезной для бизнеса игрушкой. Все больше компаний осознают ценность данного инструмента. Появился даже специальный термин – Enterprise 2.0, обозначающий комплекс методов и подходов, позволяющих применить технологии Web 2.0 для решения типичных корпоративных задач. Рынок Web-разработок не мог не отреагировать на такие тенденции, и сегодня сложно найти систему управления сайтом (CMS), которая не предоставляла бы возможность создавать социальные сети.

Компания «1С-Битрикс» в конце 2008 г. тоже выпустила модуль «Социальные сети» для своего пакета «1С-Битрикс: Управление сайтом» (БУС), который позволяет организовывать сообщества (группы), устанавливать «дружеские отношения», вести черные списки, распределять права доступа. Пользователь или группа получают развитый набор служб: блоги, фотогалереи с массовой загрузкой фотографий, рейтингами и обсуждениями, форумы, онлайновая переписка и др. Это весьма мощная разработка, более подробный обзор которой можно найти в PC Magazine/RE, 1/2009 (ознакомиться с продуктом «вживую» можно на www.pcmag.ru/club).

Модуль «Лента друзей» на сайте www.pcmag.ru/club

В этом же обзоре речь пойдет о том, чего недостает модулю «Социальная сеть» – о так называемой «ленте друзей». С точки зрения архитектуры и принципов организации данных социальный модуль БУС похож на популярную сеть FaceBook (в России более известен ее клон «ВКонтакте»). Однако в русскоязычном сегменте Сети не меньшей популярностью пользуется служба «Живой Журнал», интерфейс которой отличается от интерфейсов FaceBook и его клонов (порой радикально). Среди прочих различий в «Живом Журнале» имеется модуль «Лента друзей» (чаще «френдлента»).

Организация материала в виде «Ленты друзей» имеет ряд преимуществ. Она удобна для быстрого просмотра за чашкой утреннего кофе (или вечернего пива) новых поступлений в дружественные дневники. В БУС есть подсистема «лог обновлений», но это не совсем удачная замена. Слишком много несущественной информации и ссылок, по которым приходится лишний раз кликать. Вроде бы мелочи, но именно мелочи определяют впечатление пользователя от сайта. В общем недоработка налицо, и мы постараемся ее исправить.

Итак, постановка задачи: необходим компонент, имитирующий в БУС «Ленту друзей». «Лента друзей» конкретного пользователя как минимум должна аккумулировать новые записи из блогов друзей (полностью или в виде анонсов) и групп, в которых состоит владелец ленты. В «Ленте друзей» групп должны собираться новые записи из блогов ее участников. И, кстати, следует отметить существенное отличие блогов «Битрикс» от блогов «Живого журнала»: в «Битрикс» пользователь может публиковать сообщения не только в своем блоге и блогах групп, в которых он состоит, но и в других блогах, если владелец блога разрешил это делать.

Сначала разберемся с внутренними объектами (или сущностями) социальной сети «пользователь» и «группа»: какие свойства они имеют и как эти свойства влияют на результат. Заметим, что в обзоре будут затронуты только вопросы, касающиеся функциональности «Ленты друзей» для блогов; форумы, фотогалереи и т. д. мы во внимание не принимаем (принципы останутся теми же, а объем статьи увеличится существенно). Материал излагается в предположении, что читатель знаком с основами разработки на PHP и с API системы «1С-Битрикс: Управление сайтом».

«Пользователь»

Из свойств объекта пользователя нас интересуют настройки, которые можно задать на персональной странице социальной сети в формах настроек приватности и прав доступа к блогам. Среди настроек приватности для нашей задачи важны два поля: «кто может смотреть друзей» и «кто может смотреть мои группы». Эти поля влияют на включение в ленту записей из блогов друзей и из блогов групп, в которых состоит владелец. Причем записи из блогов друзей или групп должны включаться только, если право на просмотр имеют все пользователи. Иными словами, если Иван зайдет на страницу «Ленты друзей» Петра, а Петр разрешает просмотр своих групп только друзьям, то Иван не должен видеть записи из блогов групп Петра, даже если между ними установлены дружеские связи (в соответствующем интерфейсе модуля «Социальная сеть»).

Рис. 1. Формы настроек прав доступа объекта «пользователь»

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

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

«Группа»

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

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

Идеи и принципы

Теперь подберем функции, которые будут использоваться для выборки записей блогов с учетом прав доступа и связей пользователей внутри социальной сети. Сначала выясним, не заложены ли в системе уже готовые решения, на базе которых можно сформировать «Ленту друзей» или хотя бы нечто похожее. Первое, что приходит на ум – если есть компонент ведения журнала событий, то нет ли в нем подходящих функций? Изучаем список свойств и методов (или код компонента) и видим, что для получения списка событий в нем используется функция CSocNetLogEvents::GetUserLogEvents():

CSocNetLogEvents:: GetUserLogEvents(int userID, array arFilter = Array());

Функция возвращает структуру, содержащую список событий социальной сети по фильтру arFilter. Параметр userID – идентификатор пользователя, в массиве arFilter сохраняется набор параметров для выборки данных из БД. Он имеет структуру вида:

array(«фильтруемое поле»=>"значение фильтра" [, ...])

где фильтруемое поле может принимать специфичные значения: ENTITY_TYPE (тип сущности социальной сети, U – пользователь или G – группа), ENTITY_ID – идентификатор сущности социальной сети, EVENT_ID – идентификатор инициатора события (сигнатуры blog, photo, forum или system), LOG_DATE_DAYS – количество дней для выборки журнала. Фактически количество дней ограничено временем жизни журнала (неделя), очистка выполняется агентом CSocNetLog::ClearOldAgent().

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

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

Рис. 2. Формы настроек прав доступа объекта «группа»

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

Общая идея выглядит так. Для объектов типа «пользователь» мы получаем списки идентификаторов пользователей-друзей и идентификаторов групп, для сущности «группа» – список идентификаторов пользователей-участников. Далее из списков идентификаторов пользователей исключаем тех, чьи записи не должны войти в «Ленту друзей». Аналогично обрабатываем список идентификаторов групп. Какие именно записи из блогов не должны включаться в ленты, мы выяснили при разборе свойств объектов «пользователь» и «группа». На основе созданных списков пользователей и групп составляем списки идентификаторов блогов, извлекаем необходимые записи и передаем их в шаблон для вывода на Web-странице, откуда был вызван компонент.

Для получения списка записей в блогах будем использовать функцию CBlogPost::GetList(). На первый взгляд, больше подходит функция CBlogUser::GetUserFriendsList(), которая специально предназначена для формирования списка сообщений друзей пользователя, но, к сожалению, она опирается на связи пользователей внутри модуля блогов, а не социальной сети, а это значит, что она нам не подходит. Функция же CBlogPost::GetList() возвращает список записей, соответствующих заданному фильтру, с возможностью сортировки и разбивки на страницы.

Включение в «Ленты» записей собственных блогов (для группы или пользователя) будем рассматривать как необязательное. Для ускорения обработки запросов и уменьшения нагрузки на сервер информацию, независящую от конкретного обратившегося посетителя, будем кэшировать. Итак, основные входящие параметры:

• $arParams['USER_ID'] – ID пользователя для построения ленты;

• $arParams['GROUP_ID'] – ID группы социальной сети для построения ленты;

• $arParams['BLOG_GROUP_ID'] – ID группы блогов, к которой принадлежат все блоги социальной сети;

• $arParams['INC_SELF_MESSAGES'] – включать ли в ленту сообщения из блога сущности.

Определение базовых прав пользователя, которые заодно будут использоваться как дополнительный идентификатор кэша (см. листинг 1). Обратим внимание на структуру $arResult['CURRENT_ACCESS']. В ней задаются права, доступные текущему пользователю по умолчанию. Далее получаем его идентификатор и определяем реальное состояние прав (листинг 2).

Листинг 1

// Определим права текущего пользователя (который в данный

// момент смотрит "Ленту друзей")

$arResult = array();

// $arResult['ENTITY_TYPE'] – тип ленты,

// U – "лента пользователя", G – "лента группы"

$arResult['ENTITY_TYPE'] = $arParams['USER_ID'] >

0 ? 'U' : 'G';

// $arResult['ENTITY_ID'] – ID пользователя или группы

// (в зависимости от типа ленты)

$arResult['ENTITY_ID'] = $arResult['ENTITY_TYPE'] ==

'U' ? $arParams['USER_ID'] : $arParams['GROUP_ID'];

$arResult['CURRENT_ACCESS'] = array(

'canViewUserFriends' => false, //можно ли смотреть

// друзей пользователя

'canViewUserGroups' => false, // можно ли смотреть

// группы пользователя

'canViewUserSelfMessages' => false, // можно ли смотреть

// собственные записи

// блога пользователя

'canViewGroup' => false, // видима ли группа

'canViewGroupSelfMessages' => false // можно ли смотреть

// собственные записи

// блога группы

);

Листинг 2

$isModuleAdmin = CSocNetUser::IsCurrentUserModuleAdmin();

$currentUserID = $GLOBALS['USER']->GetID();

if($arResult['ENTITY_TYPE'] == 'G') {

// для ленты групп проверим право на доступ к ней

$arResult['GROUP_INFO'] = CSocNetGroup::GetByID($arResult['ENTITY_ID']);

$arCurrentUserPerms = CSocNetUserToGroup::InitUserPerms($currentUserID, $arResult['GROUP_INFO'], $isModuleAdmin);

$arResult['CURRENT_ACCESS']['canViewGroup'] = $arCurrentUserPerms['UserCanViewGroup'];

unset($arCurrentUserPerms);

if($arParams['INC_SELF_MESSAGES'] && $arResult['CURRENT_ACCESS']['canViewGroup']) {

$arResult['CURRENT_ACCESS']['canViewGroupSelfMessages'] = CSocNetFeaturesPerms::CanPerformOperation($currentUserID,

SONET_ENTITY_GROUP, $arResult['ENTITY_ID'], 'blog', 'view_post', $isModuleAdmin);

}

} else {

//для ленты пользователя проверим доступ к ней текущего пользователя

$arCurrentUserPerms = CSocNetUserPerms::InitUserPerms($currentUserID, $arResult['ENTITY_ID'], $isModuleAdmin);

$arResult['CURRENT_ACCESS']['canViewUserFriends'] = $arCurrentUserPerms['Operations']['viewfriends'];

$arResult['CURRENT_ACCESS']['canViewUserGroups'] = $arCurrentUserPerms['Operations']['viewgroups'];

if($arParams['INC_SELF_MESSAGES']) {

// если не нужно включать в ленту сообщения из своего блога, то и проверять ID пользователя не будем

// (экономим на количестве кэш-файлов)

$arResult['CURRENT_ACCESS']['canViewUserSelfMessages'] = $currentUserID == $arResult['ENTITY_ID'];

}

unset($arCurrentUserPerms);

}

unset($currentUserID, $isModuleAdmin);

Значение $arResult['CURRENT_ACCESS'] и будет дополнительным идентификатором кэша:

if($this->StartResultCache(false, array($arNavigation,

$arResult['CURRENT_ACCESS']), $cachePath)) {

//код компонента

//подключение шаблона сохранения результатов в кэш.

$this->IncludeComponentTemplate();

}

где $arNavigation – массив управляющих параметров для постраничной навигации, $cachePath – путь для хранения кэш-файла. Определение идентификаторов друзей и групп для получения по ним ID блогов (исходный текст несколько сокращен для обозримости, листинг 3).

Листинг 3

// массив идентификаторов пользователей, из блогов которых

// будут выбираться записи

$arEntityUsersID = array();

// массив идентификаторов групп, из блогов которых будут

// выбираться записи

$arEntityGroupsID = array();

if($arResult['ENTITY_TYPE'] == 'U') {

//лента пользователя

//можно ли смотреть друзей для данного пользователя,

// доступны ли вообще блоги и друзья

$getFriends = false;

if($arResult['CURRENT_ACCESS']['canViewUserFriends']) {

$getFriends = CPTK_SocialNetwork::IsAllowedFeature

(SONET_ENTITY_USER, 'blog') &&

CSocNetUser::IsFriendsAllowed();

}

// можно ли смотреть группы для данного пользователя

// и доступны ли вообще блоги для групп

if($arResult['CURRENT_ACCESS']['canViewUserGroups']) {

$getGroups = CPTK_SocialNetwork::IsAllowedFeature

(SONET_ENTITY_GROUP, 'blog');

}

Определим ID друзей пользователя (листинг 4).

Листинг 4

if($getFriends) {

$arOrderUR = array();

$arFilterUR = array(

'RELATION' => SONET_RELATIONS_FRIEND,

'USER_ID' => $arResult['ENTITY_ID'],

);

$arGroupByUR = false;

$arNavigationUR = false;

$arSelectFieldsUR = array(

'FIRST_USER_ID',

'SECOND_USER_ID'

);

$rsItems = CSocNetUserRelations::GetList($arOrderUR,

$arFilterUR, $arGroupByUR, $arNavigationUR,

$arSelectFieldsUR);

while($arItem = $rsItems->Fetch())

{

$id_ = $arItem['FIRST_USER_ID'] ==

$arResult['ENTITY_ID'] ?

intval($arItem['SECOND_USER_ID']) :

intval($arItem['FIRST_USER_ID']);

$arEntityUsersID[$id_] = $id_;

}

unset($rsItems, $id_, $arOrderUR, $arGroupByUR,

$arNavigationUR, $arSelectFieldsUR);

}

Код исключения «закрытых» пользователей вынесен в конец модуля (он общий для двух типов лент). Схема довольно проста: добавим ID владельца в массив $arEntityUsersID, если включен режим вывода сообщений из блога владельца ленты. Здесь же важно заполнить записью массив $arEntityUsersID на случай, если пользователь запретил смотреть своих друзей (листинг 5).

Листинг 5

if($arParams['INC_SELF_MESSAGES']) {

$arEntityUsersID[$arResult['ENTITY_ID']] =

$arResult['ENTITY_ID'];

}

// Определим ID видимых и НЕзакрытых групп пользователя

if($getGroups)

{

$arOrderGR = array();

$arFilterGR = array(

'USER_ID' => $arResult['ENTITY_ID'],

'<=ROLE' => SONET_ROLES_USER,

'GROUP_SITE_ID' => SITE_ID,

'GROUP_ACTIVE' => 'Y',

'GROUP_VISIBLE' => 'Y'

);

$arGroupByGR = false;

$arNavigationGR = false;

$arSelectFieldsGR = array(

'GROUP_ID',

'GROUP_NAME'

);

$rsItems = CSocNetUserToGroup::GetList($arOrderGR,

$arFilterGR, $arGroupByGR, $arNavigationGR,

$arSelectFieldsGR);

while($arItem = $rsItems->GetNext(false, false)) {

$id_ = intval($arItem['GROUP_ID']);

$arEntityGroupsID[$id_] = array(

'ID' => $id_,

'NAME' => $arItem['GROUP_NAME']

);

}

unset($rsItems, $id_, $arOrderGR, $arGroupByGR,

$arNavigationGR, $arSelectFieldsGR);

if(!empty($arEntityGroupsID)) {

//Определим группы, у которых блоги имеют статус

// приватных, и исключим их из списка

$arExceptGroupEntity = CPTK_SocialNetwork::

GetByRoleFeaturesIdArray(SONET_ENTITY_GROUP, 'blog',

'view_post', array('!ROLE' => SONET_ROLES_ALL),

array('!ROLE' => SONET_ROLES_ALL));

$arTmp = array_intersect_key($arExceptGroupEntity,

$arEntityGroupsID);

unset($arExceptGroupEntity);

if(!empty($arTmp)) {

foreach($arTmp as $key) {

unset($arEntityGroupsID[$key]);

}

}

unset($arTmp);

//Определим группы, в которых вообще отключены блоги,

// и исключим их из списка

$arExceptGroupEntity = CPTK_SocialNetwork::

GetByRoleFeaturesIdArray(SONET_ENTITY_GROUP, 'blog',

'view_post', array('FEATURE_ACTIVE' => 'N'),

array('FEATURE_ACTIVE' => 'N'));

$arTmp = array_intersect_key($arExceptGroupEntity,

$arEntityGroupsID);

unset($arExceptGroupEntity);

if(!empty($arTmp)) {

foreach($arTmp as $key) {

unset($arEntityGroupsID[$key]);

}

}

unset($arTmp);

}

}

Кроме того, необходимо обработать ситуацию, когда мы имеем дело с лентой для группы. Определяем, открыта ли группа для просмотра текущему пользователю, и вообще могут ли пользователи иметь блоги (листинг 6).

Листинг 6

$getMembers = false;

if($arResult['CURRENT_ACCESS']['canViewGroup']) {

$getMembers = CPTK_SocialNetwork::IsAllowedFeature

(SONET_ENTITY_USER, 'blog');

}

// Определим ID участников группы

if($getMembers) {

$arOrderGM = array();

$arFilterGM = array(

'<=ROLE' => SONET_ROLES_USER,

'GROUP_ID' => $arResult['ENTITY_ID'],

);

$arGroupByGM = false;

$arNavigationGM = false;

$arSelectFieldsGM = array(

'USER_ID'

);

$rsItems = CSocNetUserToGroup::GetList($arOrderGM,

$arFilterGM, $arGroupByGM, $arNavigationGM,

$arSelectFieldsGM);

while($arItem = $rsItems->Fetch()) {

$id_ = intval($arItem['USER_ID']);

$arEntityUsersID[$id_] = $id_;

}

unset($rsItems, $id_, $arOrderGM, $arGroupByGM,

$arNavigationGM, $arSelectFieldsGM);

// код исключения "закрытых" пользователей вынесен в конец

// модуля (он общий для двух типов лент)

}

// Добавим ID группы в $arEntityGroupsID, если включен вывод

// сообщений из блога группы

if($arParams['INC_SELF_MESSAGES'] &&

$arResult['CURRENT_ACCESS']['canViewGroup'] &&

$arResult['CURRENT_ACCESS']['canViewGroupSelfMessages']) {

//если блоги в группе не отключены

if(CSocNetFeatures::IsActiveFeature(SONET_ENTITY_GROUP,

$arResult['ENTITY_ID'], 'blog')) {

$arEntityGroupsID[$arResult['ENTITY_ID']] = array(

'ID' => $arResult['ENTITY_ID'],

'NAME' => $arResult['GROUP_INFO']['NAME']

);

}

}

Чуть выше мы отметили, что код исключения «закрытых» пользователей вынесен в конец компонента. Теперь пришло время определить пользователей, которые запретили просмотр блогов. Уберем их из списка (раз уж они сами этого хотят; листинг 7).

Листинг 7

if(!empty($arEntityUsersID)) {

$arExceptUserEntity = CPTK_SocialNetwork::

GetByRoleFeaturesIdArray(SONET_ENTITY_USER, 'blog',

'view_post', array('!ROLE' => SONET_RELATIONS_TYPE_ALL),

array('!ROLE' => SONET_RELATIONS_TYPE_ALL));

$arTmp = array_intersect_key($arExceptUserEntity,

$arEntityUsersID);

unset($arExceptUserEntity);

if(!empty($arTmp)) {

foreach($arTmp as $key)

{

unset($arEntityUsersID[$key]);

}

}

unset($arTmp);

// Если активный пользователь – владелец ленты, повторно

// включим его ID, даже если он закрыл свой блог – ему

// заведомо можно просматривать свои записи

if($arParams['INC_SELF_MESSAGES'] && $arResult

['CURRENT_ACCESS']['canViewUserSelfMessages']) {

$arEntityUsersID[$arResult['ENTITY_ID']] =

$arResult['ENTITY_ID'];

}

// Определим пользователей, которые вообще отключили свои

// блоги, и исключим их из списка

$arExceptUserEntity = CPTK_SocialNetwork::

GetByRoleFeaturesIdArray(SONET_ENTITY_USER, 'blog',

'view_post', array('FEATURE_ACTIVE' => 'N'),

array('FEATURE_ACTIVE' => 'N'));

$arTmp = array_intersect_key($arExceptUserEntity,

$arEntityUsersID);

unset($arExceptUserEntity);

if(!empty($arTmp)) {

foreach($arTmp as $key) {

unset($arEntityUsersID[$key]);

}

}

unset($arTmp);

}

Здесь надо обратить внимание на две дополнительные функции, которые не входят в стандартный API ядра «1С-Битрикс: Управление сайтом»: CPTK_SocialNetwork::

GetByRoleFeaturesIdArray() и CPTK_SocialNetwork::IsAllowedFeature(). Они представляют собой часть библиотеки автора, их текст здесь не приводится (при желании библиотеку можно запросить у автора статьи). Первая, CPTK_SocialNetwork::GetByRoleFeaturesIdArray(), возвращает массив идентификаторов объектов по типу объекта, сигнатуре, функциональности и операциям (с возможностью установки дополнительного фильтра). Функция имеет внутреннее кэширование результатов, в ее основе лежит метод CSocNetFeaturesPerms::GetList(). Вторая, CPTK_SocialNetwork::IsAllowedFeature(), проверяет, доступна ли для заданного объекта затребованная возможность (это небольшая оптимизация часто выполняемой операции, опытному разработчику не составит труда реализовать ее самостоятельно).

На этом этапе возникает проблема: при организации группы социальной сети система не создает соответствующие записи в таблицах БД, где хранятся настройки прав доступа к заданным функциям. Эти записи автоматически создаются только после первого изменения прав доступа, до того они определяются системой, средствами PHP. Причем по умолчанию используется режим не «разрешено всем», а «разрешено только участникам». Следовательно, попытка выбрать все записи, у которых в поле ROLE не установлено значение константы SONET_RELATIONS_TYPE_ALL («разрешено всем»; в нашем случае это и будет вызов CPTK_SocialNetwork::GetByRoleFeaturesIdArray() с установленным дополнительным фильтром array('!ROLE' => SONET_RELATIONS_TYPE_ALL), приведет к получению неверного результата. Этот нюанс можно считать досадной ошибкой в архитектуре модуля, но, к счастью, дело поправимо без вмешательства в ядро системы. В нашем случае оказалось достаточно добавить обработчик события OnSocNetGroupAdd, где и выполняются необходимые для корректной записи в БД структуры прав операции. В группах, которые были созданы ранее, эта ошибка была исправлена с помощью «Мастера».

Двигаемся дальше – получаем массив блогов, из которых будут выбираться новые записи (листинг 8).

Листинг 8

$arBlogID = array(); // массив блогов

if(!empty($arEntityUsersID)) { // по владельцу блога

$arOrderBlog = array();

$arFilterBlog = array(

'OWNER_ID' => array_keys($arEntityUsersID),

'GROUP_ID' => $arParams['BLOG_GROUP_ID'],

'GROUP_SITE_ID' => SITE_ID,

'ACTIVE' => 'Y'

);

unset($arEntityUsersID);

$arGroupByBlog = false; $arNavigationBlog = false;

$arSelectFieldsBlog = array('ID', 'SONET_GROUP_ID');

$rsItems = CBlog::GetList($arOrderBlog, $arFilterBlog,

$arGroupByBlog, $arNavigationBlog, $arSelectFieldsBlog);

while($arItem = $rsItems->Fetch()) {

if(intval($arItem['SONET_GROUP_ID']) <= 0) {

$id_ = intval($arItem['ID']);

$arBlogID[$id_] = $id_;

}

}

unset($rsItems, $id_, $arOrderBlog, $arGroupByBlog,

$arNavigationBlog, $arSelectFieldsBlog);

}

if(!empty($arEntityGroupsID)) { // по группе блога в соцсети

$arOrderBlog = array();

$arFilterBlog = array(

'SOCNET_GROUP_ID' => array_keys($arEntityGroupsID),

'GROUP_ID' => $arParams['BLOG_GROUP_ID'],

'GROUP_SITE_ID' => SITE_ID,

'ACTIVE' => 'Y'

);

$arGroupByBlog = false; $arNavigationBlog = false;

$arSelectFieldsBlog = array('ID', 'OWNER_ID');

$rsItems = CBlog::GetList($arOrderBlog, $arFilterBlog,

$arGroupByBlog, $arNavigationBlog, $arSelectFieldsBlog);

while($arItem = $rsItems->Fetch()) {

if(intval($arItem['OWNER_ID']) <= 0) {

$id_ = intval($arItem['ID']);

$arBlogID[$id_] = $id_;

}

}

unset($rsItems, $id_, $arOrderBlog, $arGroupByBlog,

$arNavigationBlog, $arSelectFieldsBlog);

}

Здесь все вполне прозрачно, комментарии не требуются. И наконец, выбираем записи из блогов, которые и станут основой для формирования конечного результата – «Ленты друзей» (листинг 9).

Листинг 9

if(!empty($arBlogID)) {

$arFilter = array(

'BLOG_ACTIVE' => 'Y',

'BLOG_GROUP_SITE_ID' => SITE_ID,

'PUBLISH_STATUS' => BLOG_PUBLISH_STATUS_PUBLISH,

'BLOG_ID' => array_keys($arBlogID),

'ACTIVE' => 'Y'

);

unset($arBlogID);

if($arParams['MAX_DAYS_COUNT'] > 0) {

// задан промежуток времени для выборки сообщений 86400 —

// кэшируем на сутки

$from = intval(time() – $arParams['MAX_DAYS_COUNT']*86400);

$arFilter['>=DATE_PUBLISH'] = ConvertTimeStamp($from,

'FULL');

}

$arGroupBy = false;

$arSelectFields = array(

'ID',

'BLOG_ID',

'TITLE',

'DATE_PUBLISH',

'AUTHOR_ID',

'DETAIL_TEXT',

'BLOG_ACTIVE',

'BLOG_URL',

'BLOG_GROUP_ID',

'BLOG_GROUP_SITE_ID',

'AUTHOR_LOGIN',

'AUTHOR_NAME',

'AUTHOR_LAST_NAME',

'BLOG_USER_ALIAS',

'BLOG_OWNER_ID',

'BLOG_USER_AVATAR',

'NUM_COMMENTS',

'VIEWS',

'ATTACH_IMG',

'BLOG_SOCNET_GROUP_ID'

);

$rsItems = CBlogPost::GetList($arOrder, $arFilter,

$arGroupBy, $arNavParams, $arSelectFields);

$rsItems->bShowAll = $arParams['PAGER_SHOW_ALL'];

//создаем объект парсера сообщений блогов

$obParser = new blogTextParser(false,

$arParams['PATH_TO_SMILE']);

while($arItem = $rsItems->GetNext()) {

// здесь код разбора записи блога – ссылки, аватары,

// картинки, выполняем парсинг текста сообщения и т. д.

}

unset($obParser, $arOrder, $arGroupBy, $arSelectFields);

unset($arEntityGroupsID);

}

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

Архитектура «Ленты друзей»: проблемы и решения

Легко заметить, что данный компонент не столь совершенен, каким мог бы быть. Скажем, напрашивается вопрос: а нельзя ли в «Ленте друзей» учитывать структуру связей пользователя с группами и другими пользователями социальной сети? Теоретически можно, на практике нагрузка на сервер возрастет в разы (если не на порядки), причем кэшировать что-либо будет невозможно. Причина – необходимость учитывать огромное количество комбинаций настроек, слишком много факторов будут определять итоговый результат.

Посмотрим, как будет влиять на содержимое «Ленты друзей» сложная конфигурация прав доступа. Предположим, что в профиле пользователя имеются настройки:

• «просматривать список друзей могут только друзья» – это означает, что прежде чем включать записи, опубликованные друзьями владельца ленты в блогах социальной сети, нужно выяснить, не является ли «текущий пользователь» («текущий пользователь» – пользователь, который в данный момент смотрит ленту) другом владельца ленты;

• «просматривать список друзей могут только друзья и друзья друзей» – прежде чем включать записи, опубликованные друзьями владельца ленты, нам нужно выяснить, не является ли текущий пользователь другом или другом друга владельца ленты;

• «просматривать список друзей могут все пользователи» – включаем записи, опубликованные друзьями владельца ленты в блогах социальной сети;

• «полный запрет на просмотр друзей» – не включаем записи, опубликованные друзьями владельца ленты в блогах социальной сети.

Настройки доступа к блогам пользователя:

• «просматривать сообщения могут все пользователи» – включаем записи блога в какую-либо ленту;

• «просматривать сообщения могут только друзья пользователя» – прежде чем включить записи блога в какую-либо ленту, необходимо проверить, не является ли текущий пользователь другом владельца блога;

• «просматривать сообщения могут только друзья и друзья друзей пользователя» – прежде чем включить записи блога в какую-либо ленту, необходимо проверить, не является ли текущий пользователь другом или другом друга владельца блога;

• «просматривать сообщения может только владелец блога» – прежде чем включить записи блога в какую-либо ленту, необходимо проверить, не является ли текущий пользователь владельцем этого блога.

Настройки приватности группы:

• «группа видима всем посетителям» – включаем записи, опубликованные в блоге группы без проверки членства текущего пользователя в данной группе;

• «группа не видима всем посетителям» – прежде чем включить записи, опубликованные в блоге группы, нужно проверить, является ли текущий пользователь ее членом.

Настройки доступа к блогам группы:

• «просматривать сообщения блога могут только члены группы» – это означает, что, прежде чем включать записи, опубликованные в блоге группы, нам нужно выяснить, не является ли текущий пользователь ее членом;

• «просматривать сообщения блога могут только владелец группы и модераторы» – прежде чем включать записи, опубликованные в блоге группы, нужно выяснить, является ли пользователь ее владельцем или модератором;

• «просматривать сообщения блога могут все пользователи» – включаем записи блога группы без дополнительной проверки прав;

• «просматривать сообщения блога может только владелец группы» – прежде чем включать записи, опубликованные в блоге группы, нам нужно выяснить, не является ли текущий пользователь ее владельцем.

Все это придется проверять для каждого (!) блога, который будет попадать в чью-либо ленту. Наглядный пример. Допустим, Иван состоит в группе «Любители виски», которая видима всем посетителям сайта, но сообщения блогов могут читать только члены группы, и свой блог Иван разрешает читать только своим друзьям. Петя состоит в группе «Любители молока», которая видима всем посетителям сайта, и сообщения блогов открыты для всех. Петя – друг Ивана и читать сообщения из своего блога тоже разрешает только друзьям. Маша не состоит в указанных группах и сообщения из своего блога разрешает читать всем посетителям сайта. При этом Маша – друг Пети.

Теперь, если Петя захочет почитать ленту Ивана, то ему должны быть доступны только сообщения из блога Ивана. Если же Иван будет читать ленту Пети, то он должен видеть сообщения из блога Пети и из блога группы «Любителей молока». Маша, посетив ленту Ивана, вообще не должна видеть сообщений, а в ленте Пети – видеть только сообщения из группы «Любителей молока». Если Иван или Маша посетят ленту группы «Любителей молока», то они должны видеть сообщения из блога группы и сообщения из блога Пети. В ленте Маши, Вася и Петя должны будут видеть только сообщения из блога Маши.

Таким образом, для каждого посетителя каждой ленты придется генерировать уникальный кэш, что совершенно противопоказано для метода «полного кэширования результата» (когда сохраняется полностью готовый результат и на время жизни кэша он выдается без единого запроса к базе данных и вычислений в рамках логики компонента). Если предположить, что каждая лента будет состоять, скажем, из 10 страниц, а всего активных участников социальной сети (без учета групп!), например, 1000, то только для лент пользователей будет генерироваться 10 страниц ленты × 1000 лент × 1000 пользователей = 10 000 000 кэш-файлов. Если каждый кэш-файл будет занимать порядка 30 000 байт дискового пространства, то суммарный объем кэш-файлов только лент будет составлять 10 000 000 × 30 000 = 300 000 000 000 байт (≈279 Гбайт)! Мягко говоря, немало.

Как вариант, можно было бы использовать метод генерации страниц ленты с частичным, но не значимым кэшированием. Но полумеры не спасают. Нагрузка на сервер возрастает, несущественно снижая объем кэша (в экспериментах – примерно в три раза) и радикально усложняя логику обработки (как минимум придется тщательно заботиться о сбросе некорректного кэша), отладку и сопровождение модуля.

В итоге при создании социальной сети для сайта www.pcmag.ru было принято решение реализовать ленту примерно в том виде, как описано в данной статье (ряд мелких деталей опущен для удобочитаемости). Да и, как показывает практика, особенной надобности в подключении к «Ленте друзей» дополнительных сущностей в общем-то не возникает. При необходимости (например, если все пользователи вдруг дружно возжелали видеть фотографии друг друга) компонент может быть доработан и расширен.

Drupal: разработка модуля

Роман Архаров

Данная статья – продолжение материала, посвященного CMS Drupal (см. PC Magazine/RE, 12/2008). В первой статье подробно рассказано о назначении и возможностях системы, а также приведены примеры сборки сайтов на Drupal с использованием уже существующих модулей. Этот же материал будет больше интересен техническим специалистам, умеющим программировать на языке PHP, знакомым с основами HTML и CSS, и тем, кто хочет больше узнать о методах разработки собственных модулей для этой системы. Предыдущая статья доступна сейчас в Интернете по адресу: www.pcmag.ru/solutions/detail.php?ID=32535. Перед чтением этого материала рекомендуется освежить в памяти информацию, просмотрев ее первые три раздела.

Разработка собственного модуля

Система управления сайтом Drupal построена по модульному принципу: компактный набор служебных функций (ядро) расширяется при помощи модулей – файлов с PHP-кодом. Модули должны содержать «хуки» (hooks) – особым образом именованные функции, которые вызываются ядром Drupal при возникновении каких-либо событий. Каждый модуль имеет системное имя, которое должно состоять из латинских букв, цифр, знака подчеркивания (и начинаться обязательно с буквы). Имя хука должно состоять из двух частей: имени модуля и названия события. При возникновении любого события ядро Drupal в каждом из установленных модулей ищет и выполняет соответствующую функцию, т. е. функцию с именем название_модуля_название_события. Например, при возникновении событий, связанных с учетной записью пользователя (регистрация, авторизация, изменение роли пользователя и др.), ядро Drupal вызывает функции, реализующие хук hook_user, поэтому, чтобы модуль с именем example мог отреагировать на это событие, в нем необходимо объявить функцию с именем example_user(). Список передаваемых в эту функцию аргументов, пример ее использования и информацию обо всех функциях и хуках, доступных в Drupal, можно найти на странице официальной документации http://api.drupal.org или ее русской версии: http://api.drupal.ru.

Каждый модуль для Drupal представляет два файла или более, которые должны находиться в папке sites/all/modules/название_модуля[1].

В файле название_модуля.info должна находиться служебная информация, а в файле название_модуля.module – исходный текст. При наличии этих двух файлов модуль станет доступным на странице установки модулей Drupal (Administer – Modules, admin/build/modules). Кроме того, в этой же папке может находиться необязательный файл название_модуля.install, содержащий реализации хуков, которые будут выполнены при инсталляции модуля. В этом файле обычно располагаются инструкции, создающие новые таблицы в базе данных и задающие значения по умолчанию для настроек модуля.

Для иллюстрации использования системы хуков я приведу пример разработки простого модуля, который формирует блок с информацией о курсах валют. Этот модуль при выполнении соответствующей строки cron-таблицы будет соединяться с сервером ЦБ РФ и получать от него информацию о курсах валют. На основе полученных данных и настроек, заданных через интерфейс управления модулем, а также функций темизации будет генерироваться выходный HTML-код. Он будет кэшироваться стандартными средствами Drupal (благодаря чему администратор при желании сможет перенести этот кэш из базы данных, например, в файловую систему или memcache) и выдаваться по запросу пользователя в виде блока.

Модуль получит название currencies. На первом этапе его разработки необходимо (относительно корня Drupal-сайта) создать папку sites/all/modules/currencies, в которой мы будем сохранять новые файлы.


currencies.info

В .info-файлах модулей содержится служебная информация, без которой модуль не будет виден в системе. Начинаться любой .info-файл должен со строки

; $Id$

В файлах с PHP-кодом после открывающего тега

// $Id$

Эту строку, если модуль будет размещен в официальном CVS-репозитории Drupal, заменит служебная информация.

Далее в файле .info должны располагаться три обязательных параметра: название модуля, его описание и версия ядра Drupal, с которой работает модуль. Кроме того, в этом файле могут находиться необязательные параметры: минимальная версия PHP, необходимая для запуска модуля, зависимость от других модулей Drupal, без которых текущий модуль не будет работать, и пр. Подробное описание всех доступных к использованию в .info-файле параметров можно найти в официальной документации (ссылка на эту и другие цитируемые в статье страницы документации размещена во врезке «Ссылки на документацию»).

В нашем случае файл currencies.info будет иметь такой вид:

; $Id$

name = Currencies block

description = Show currencies

core = 6.x

Хотя один файл в нашем модуле уже есть, но пока он отсутствует в списке имеющихся в системе модулей, и мы переходим к следующему файлу.


currencies.install

По стандартам кодирования Drupal перед каждой функцией, реализующей хук, должен быть размещен комментарий вида:

/*

* Implementation of hook_название_хука().

*/

Аналогичный комментарий (только без текста Implementation of...) с описанием функции и ее аргументов должен располагаться перед любой другой функцией. Эти комментарии используются системой генерации документации Doxygen, по ним также удобно вести поиск. Здесь для экономии места такие комментарии опущены.

Система управления сайтом Drupal построена по модульному принципу: ядро расширяется при помощи модулей – файлов с PHP-кодом.

При инсталляции и деинсталляции модулей вызываются хуки hook_install и hook_uninstall. Отмечу, что в Drupal кроме понятий инсталляции и деинсталляции есть понятия активации и деактивации модуля. Если модуль устанавливается впервые (в административном интерфейсе, в списке модулей установлена галочка напротив нужного модуля и нажата кнопка Submit), сначала происходит событие install, затем событие enable, т. е. ядро Drupal ищет и, если находит, вызывает функции, реализующие хуки hook_install и hook_enable для устанавливаемого модуля. Далее, если администратор выключает модуль, то происходит событие disable и вызывается функция, реализующая хук hook_disable. В следующий раз, когда модуль будет вновь включен, произойдет только событие enable, а не install. Если модуль был сначала деактивирован, а затем удален (удаление производится на отдельной от списка модулей странице), то происходят события disable и uninstall и в следующий раз при включении модуля опять произойдут события install и enable.

Такое разделение очень удобно. Обычно при возникновении события install программисты создают необходимые для работы модуля таблицы в базе данных, а при событии uninstall – удаляют их, таким образом после деинсталляции модуля в системе не остается никаких свидетельств его присутствия. При включении и выключении модуля (enable и disable) никакие сохраненные модулем данные не удаляются, а лишь отключается функционал модуля.

При возникновении события install программисты создают необходимые для работы модуля таблицы в базе данных

Вернемся к нашему примеру. Информацию о курсах валют разрабатываемый модуль currencies будет получать с сервера Центрального Банка РФ. Чтобы при каждом показе блока не обращаться с запросом к удаленному серверу, данные будут сохраняться в базе данных нашего сайта. В самом простом случае для хранения данных можно было бы воспользоваться функциями cache_set или variable_set из ядра Drupal, однако такой подход не очень удобен, когда нужно хранить информацию о курсах валют за длительный период, например, для последующего ее анализа. Поэтому мы создадим отдельную таблицу в базе данных и в ней будем хранить всю полученную информацию.

Наш пример довольно прост, поэтому события enable и disable использоваться в нем не будут, а на install и uninstall мы назначим функции создания и удаления таблицы в БД, для чего в файле currencies.install разместим функции, реализующие хуки hook_install и hook_uninstall:

function currencies_install() {

drupal_install_schema('currencies');

}

function currencies_uninstall() {

drupal_uninstall_schema('currencies');

}

Функции drupal_install_schema и drupal_uninstall_schema являются частью Drupal Schema API. Schema API – это слой абстракции от базы данных, благодаря которому программист может не задумываться о том, как адаптировать свой SQL-запрос под ту или иную базу данных, ему достаточно сформировать массив определенного вида и передать его одной из функций Schema API, после чего этот массив будет преобразован в корректный SQL-запрос к той базе данных, которая используется с Dupal. Единственный аргумент, который принимают эти две функции, – название модуля, схема которого будет установлена или удалена, т. е. после вызова drupal_install_schema('currencies') Drupal попытается создать таблицы, описанные в реализации hook_schema модуля currencies, поэтому нужно создать эту реализацию (см. листинг 1). Этот хук должен возвращать ассоциативный массив, содержащий информацию о создаваемых таблицах. В нашем примере создается таблица с именем currencies_block и двумя полями: timestamp и data. Подробное описание формата возвращаемого массива можно найти в документации.

Листинг 1

function currencies_schema() {

$schema['currencies_block'] = array(

'description' => t('Some table description.'),

'fields' => array(

'timestamp' => array(

'type' => 'int',

'size' => 'normal',

'not null' => TRUE,

'default' => 0,

),

'data' => array(

'type' => 'text',

'not null' => TRUE,

),

),

);

return $schema;

}

На данном этапе в нашем модуле currencies есть два файла: currencies.info и currencies.install, но он по-прежнему недоступен для выбора на странице со списком модулей, поскольку в нем отсутствует самый важный файл – .module. Если в папке модуля создать файл currencies.module и разместить в нем всего две строчки:

// ; $Id$

(их описание было дано выше), модуль тут же станет доступным для установки, однако, так как файл currencies.module не содержит никаких инструкций, установка этого модуля приведет только к созданию одной таблицы в БД и он не будет нам полезен. Поэтому мы переходим к самому большому, сложному и важному этапу – разработке основного функционала модуля.


currencies.module

В Drupal реализован механизм «ролей» и «прав доступа». По умолчанию в системе есть две «роли» (группы) пользователей – анонимы и авторизованные пользователи, одна из которых автоматически присваивается каждому пользователю в зависимости от того, авторизован он в системе или нет. Каждый модуль может объявить «права доступа» к своим ресурсам, после чего администратор сайта через специальный интерфейс может установить, какие роли имеют доступ к каким сервисам сайта. Например, обычно пользователи, входящие в группу авторизованных, могут вести персональный блог, а у анонимов такой возможности нет. В любой момент администратор сайта через Web-интерфейс может создать дополнительные роли (модераторы, администраторы и т. п.), определить особые разрешения для каждой из ролей (например, модераторы могут снимать с публикации материалы и комментарии, а администраторы их безвозвратно удалять) и назначать эти роли любому пользователю, причем ему одновременно может быть присвоено несколько ролей.

В интерфейсе администратора с помощью hook_menu и Forms API будет создана форма, позволяющая указать список валют.

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

• интерфейс администратора;

• автоматически выполняемые процедуры;

• функции темизации;

• интерфейс пользователя.

В интерфейсе администратора с помощью hook_menu и Forms API будет создана форма, позволяющая администратору указать список валют, которые необходимо выводить в блоке, адрес XML-документа, из которого будет «подтягиваться» информация об обновленных курсах валют, и частоту обновления данных. Кроме того, с помощью hook_perm будет создано «право доступа», дающее возможность администратору сайта ограничить доступ к настройкам модуля.

Регулярно выполняемая процедура в модуле будет одна: при запуске строки cron-таблицы необходимо проверить, когда произошло последнее обновление данных о курсах валют. Если между текущим моментом и последним обновлением данных прошло время большее, чем указано в соответствующей настройке интерфейса управления модулем, то необходимо соединиться с сервером ЦБ, получить обновленные данные и сохранить их в базе.

Интерфейс пользователя создадим при помощи хука hook_block, позволяющего формировать блоки с данными. Чтобы верстальщик мог изменять внешний вид выводимых модулем данных, создадим свою функцию темизации, которую необходимо зарегистрировать в системе при помощи hook_theme.

Административный интерфейс

Теперь приступим к реализации каждого из описанных этапов. Для начала добавим в наш файл currencies.module функцию:

function currencies_perm() {

return array('access currencies block settings');

}

Эта функция – реализация хука hook_perm, который, как сказано выше, определяет дополнительные права доступа. Hook_perm – один из самых простых хуков, он всего лишь возвращает массив строк, представляющих собой права доступа. После инсталляции модуля администратор сайта на странице Admin – Permissions (admin/user/permissions) может указать, какие группы пользователей имеют право доступа access currencies block settings, а мы в дальнейшем, во время реализации формы настроек модуля при помощи функции user_access, будем проверять, имеет ли текущий пользователь право доступа access currencies block settings или нет.

Важный момент. Пользователь с uid=1, т. е. первый созданный в системе, является суперпользователем, для него функция user_access всегда возвращает значение TRUE, а это значит, что он всегда имеет доступ ко всем функциям сайта. Это одна из причин, почему не рекомендуется работать в системе с учетной записью суперпользователя: зачастую разработчики забывают раздавать пользователям необходимые права доступа, так как сами, работая как суперпользователи, не имеют проблем с доступом к ресурсам сайта.

Для определения дополнительного системного пути, по которому в нашем примере будет доступна страница управления модулем, необходимо создать реализацию хука hook_menu:

function currencies_menu() {

$items = array();

$items['admin/settings/cur-block'] = array(

'title' => t('Currencies block settings'),

'description' => 'Currencies block settings.',

'page callback' => 'drupal_get_form',

'page arguments' => array('currencies_settings'),

'access arguments' => array('access cur block settings'),

);

return $items;

}

Эта функция также возвращает ассоциативный массив. Ключом каждого элемента массива должен быть путь, регистрируемый в системе (в нашем случае это admin/settings/cur-block), а значением – вложенный массив, содержащий информацию о создаваемом пункте меню. Давайте разберем каждый из параметров отдельно.

Title – заголовок меню – будет использоваться при переходе на страницу с адресом admin/settings/cur-block в строке заголовка браузера (тег ) и в качестве заголовка страницы (тег <h1>), а также в качестве текста ссылки, ведущей на созданную страницу настроек.</p> <p>Description – описание пункта меню, которое в нашем случае будет использоваться на странице администрирования.</p> <p>Page callback – функция, которая будет генерировать страницу, создаваемую по указанному пути. В простом случае значением этого параметра должна быть функция, возвращающая HTML-код, который будет показан пользователю. Однако мы по указанному адресу создаем не обычную страницу, а форму, значения которой автоматически сохраняются в БД. Поэтому для параметра page callback мы назначаем вызов системной функции drupal_get_form(), которая выведет на экран форму, созданную функцией с именем, указанным в элементе массива page arguments; в нашем случае это функция currencies_settings(). Функция currencies_settings() должна вернуть ассоциативный массив, содержащий информацию об элементах создаваемой формы. Подробнее об этом массиве будет рассказано ниже.</p> <p>Access arguments – массив «прав доступа». Пользователи, обладающие правами доступа, перечисленными в этом массиве, могут получить доступ к создаваемому пункту меню.</p> <div class='cite'><div class='subtitle'>Подготовка Web-страницы</div><p>Тема оформления в Drupal – это набор особым образом сформированных HTML-шаблонов и CSS-файлов, на основе которых ядро Drupal генерирует запрашиваемую пользователем страницу. Если в системе используется встроенный в Drupal шаблонный «движок» PHPTemplate, то каждая тема оформления может содержать служебный файл template.php, в котором могут размещаться функции, переопределяющие стандартный вывод модулей. У каждой темы оформления, как и у каждого модуля, должно быть свое уникальное имя и файл настроек .info.</p> </div><p>Более подробную информацию о параметрах пунктов меню можно найти в документации.</p> <p>Сейчас в нашем модуле определен новый пункт меню, но не определена функция, формирующая содержимое страницы, на которую этот пункт указывает (см. листинг 2).</p> <div class='cite'><div class='subtitle'>Листинг 2</div><p>function currencies_settings() {</p> <p> $form['currencies_list'] = array(</p> <p> '#type' => 'textfield',</p> <p> '#title' => t('Currencies'),</p> <p> '#default_value' => variable_get('currencies_list',</p> <p> "USD,EUR,CNY,BYR,KZT,TRY,UAH,JPY"),</p> <p> '#maxlength' => 255,</p> <p> );</p> <p> $form['currencies_list_freq'] = array(</p> <p> '#type' => 'textfield',</p> <p> '#title' => t('Frequency of updating of the data</p> <p> (in seconds)'),</p> <p> '#default_value' => variable_get('currencies_list_freq',</p> <p> 3600),</p> <p> '#maxlength' => 255,</p> <p> '#description' => t('It is recommended to use value</p> <p> not less than 3600.'),</p> <p> );</p> <p> $form['currencies_list_url'] = array(</p> <p> '#type' => 'textfield',</p> <p> '#title' => t('Адрес xml-файла'),</p> <p> '#default_value' => variable_get('currencies_list_url',</p> <p> "http://www.cbr.ru/scripts/XML_daily.asp?date_req=</p> <p> %d/%m/%y"),</p> <p> '#maxlength' => 255,</p> <p> '#description' => t('The XML-file address.'),</p> <p> );</p> <p> return system_settings_form($form);</p> <p>}</p> </div><p>Как и хуки hook_menu, hook_schema и многие другие хуки Drupal, эта функция должна возвращать ассоциативный массив, на этот раз содержащий информацию о параметрах создаваемой формы. Здесь мы создаем три однострочных текстовых поля (параметр #type имеет значение textfield), значения по умолчанию для которых (параметр #default_value) будут храниться и выбираться из стандартной таблицы variables Drupal при помощи функций variable_set() и variable_get(). Благодаря использованию функций drupal_get_form и system_settings_form нет необходимости заботиться о создании кнопок Submit и Reset, а также о функциях, обрабатывающих и сохраняющих данные формы. В более сложных случаях, которые будут рассмотрены в следующей статье, придется вручную создавать функции проверки введенных пользователем значений и сохранения данных.</p> <p>Подробное описание типов полей, используемых в формах, можно найти в документации.</p> <p>Все, мы завершили разработку первой из трех частей нашего модуля – административного интерфейса и переходим к разработке второй его части – инструмента получения данных от удаленного сервера.</p> <div class='subtitle'>Регулярные процедуры</div><p>Чтобы Drupal периодически выполнял определенные действия, в планировщике задач операционной системы необходимо настроить запуск файла cron.php, который находится в корне каждого Drupal-сайта. При выполнении этого файла будет вызываться хук hook_cron, и в нашем модуле мы напишем его реализацию:</p> <div class='cite'><p>function currencies_cron() {</p> <p> currencies_contents();</p> <p>}</p> </div><p>Процедура получения и обработки XML-файла, расположенного на удаленном сервере, довольно обычна, поэтому она здесь не приведена. При желании вы можете самостоятельно разобрать логику работы этой функции, изучив исходные коды модуля Currencies, которые есть на диске, прилагаемом к журналу. В результате ее работы формируется массив $result, в котором содержится информация о курсах валют и который передается функции темизации. Функцию currencies_contents() см. в листинге 3. Здесь мы сначала проверяем, нет ли запрашиваемых данных в кэше Drupal, если нет, то происходит соединение с удаленным сервером (его адрес указан через административный интерфейс модуля), получение и обработка XML-файла и формирование массива данных, который передает функции темизации. Функция темизации формирует выходной HTML-код, записываемый в кэш, его время жизни явно указывается исходя из соответствующей настройки, заданной в интерфейсе управления модулем. Drupal автоматически управляет закэшированными данными и при необходимости удаляет устаревшие записи.</p> <div class='cite'><div class='subtitle'>Листинг 3</div><p>function currencies_contents() {</p> <p> if(!$c = cache_get('currencies')) {</p> <p> /* здесь пропущен код, отвечающий за получение</p> <p> и обработку XML-файла */</p> <p> $output = theme('currencies_block', $result);</p> <p> $t = variable_get('currencies_list_freq', 3600);</p> <p> if(!$t || !is_numeric($t)) $t = 3600;</p> <p> cache_set("currencies", $output, 'cache', time() + $t);</p> <p> } else {</p> <p> $output = $c->data;</p> <p> }</p> <p> return $output;</p> <p>}</p> </div><p>Функция currencies_contents() будет использоваться не только при запуске cron-задания, но и при формировании блока с данными, который будет показываться пользователю. Таким образом, практически всегда пользователи будут видеть данные, полученные из кэша Drupal, если же на сайте не работает cron и нет закэшированных данных о курсах валют, то произойдет соединение с сервером ЦБ, формирование и запись в кэш необходимых данных.</p> <div class='subtitle'>Функции темизации</div><p>Теперь подробнее остановимся на функциях темизации (theming; термин не слишком благозвучен, но уже стал общепринятым). Теоретически прямо в коде функции currencies_contents() можно было бы сформировать HTML-код, который в дальнейшем и видел бы посетитель сайта в браузере. Однако такой подход неверен, поскольку при изменении оформления данных пришлось бы менять код модуля, а это влечет за собой две проблемы. Во-первых, модуль могут использовать сторонние разработчики, и им для внесения изменений придется вникнуть в структуру модуля и внести в него изменения, которые могут привести к ошибкам. Во-вторых, часто при разработке крупных проектов версткой и программированием занимаются разные люди. Верстальщик может не иметь представления о том, как работать с языком PHP в целом и модулями Drupal в частности. По этому правильнее вынести все действия, связанные с оформлением данных, в отдельные файлы и функции, для чего и нужны функции темизации.</p> <div class='cite'><p>Функции темизации – это функции, генерирующие HTML-код, который впоследствии показывается пользователю.</p> </div><p>Функции темизации – это функции, генерирующие HTML-код, который впоследствии показывается пользователю. Особенность таких функций в том, что, во-первых, они не должны реализовывать никакой бизнес-логики, т. е. в их задачи входит только генерирование HTML-кода на основе полученных аргументов. Во-вторых, эти функции могут быть переопределены разработчиком сайта без редактирования кода модуля, путем изменения файла template.php или создания файла-шаблона.</p> <p>В нашем случае массив данных формируется функцией currencies_contents(), а HTML-код создается функцией theme_currencies_block(), которую нам сейчас предстоит определить и зарегистрировать. Подчеркну, что вызов любой функции темизации осуществляется через вызов функции-обертки с именем theme(), т. е. функция theme_currencies_block($argument) должна вызываться как theme('currencies_block', $argument). Чем вызвана эта необходимость, я объясню чуть позже.</p> <p>Для регистрации функций темизации, используемых в модуле, мы должны реализовать hook_theme, который возвращает массив имен применяемых функций темизации и принимаемых ими параметров. В нашем модуле будет использоваться одна функция темизации theme_currencies_block(), которая на вход принимает один обязательный аргумент – массив курсов валют, поэтому реализация хука hook_theme будет выглядеть так:</p> <div class='cite'><p>function currencies_theme() {</p> <p> return array(</p> <p> 'currencies_block' => array(</p> <p> 'arguments' => array('result' => NULL),</p> <p> ),</p> <p> );</p> <p>}</p> </div><p>Без такой регистрации вызов theme('currencies_block', $argument) будет невозможен. Сама функция темизации будет такой:</p> <div class='cite'><p>function theme_currencies_block($result) {</p> <p> $output = "<ul>";</p> <p> foreach($result as $k => $v) {</p> <p> if($v["diff"] > 0) {</p> <p> $color = "#5aaf43";</p> <p> $v["diff"] = "+" . $v["diff"];</p> <p> }</p> <p> else if($v["diff"] < 0) $color = "#f00";</p> <p> else if($v["diff"] == 0) $color = "#00f";</p> <p> $output .= "<li>" . $v["nominal"] . " " . $v["name"] . " = "</p> <p> . $v["value"] . " (<span style=\"color: " . $color . "\">"</p> <p> . $v["diff"] . "</span>)</li>";</p> <p> }</p> <p> $output .= "</ul>";</p> <p> return $output;</p> <p>}</p> </div><p>Еще раз обратите внимание на то, что в функции currencies_theme в качестве имени функции темизации указывается название currencies_block, а реализация ее имеет имя theme_currencies_block(). Нужно это, чтобы в дальнейшем разработчики сайтов, использующие наш модуль, могли переопределить эту функцию, т. е. изменить формируемый ею HTML-код. Для этого им в папке со своей темой в файле template.php достаточно будет создать функцию с именем, совпадающим с именем функции темизации, но в котором слово theme заменено на название используемой темы оформления, т. е. создать функцию название_темы_оформления_currencies_block(). Ядро Drupal, когда встретит в коде модуля вызов вида theme('currencies_block', $result), сначала попробует найти функцию темизации в файле template.php используемой в данный момент темы оформления, и только если там ее не найдет, использует функцию theme_currencies_block().</p> <div class='cite'><p>Ядро Drupal пытается найти функцию темизации в файле template.php используемой в данный момент темы оформления.</p> </div><p>В случае больших шаблонов удобнее вынести функцию темизации из файла template.php в отдельный файл. Для этого в массиве, возвращаемом реализацией хука hook_theme, нужно добавить элемент с ключом template и именем, соответствующим имени файла-шаблона, а в каталоге с модулем разместить файл-шаблон с указанным ранее именем и расширением .tpl.php. В итоге hook_theme() примет вид:</p> <div class='cite'><p>function currencies_theme() {</p> <p> return array(</p> <p> 'currencies_block' => array(</p> <p> 'arguments' => array('result' => NULL),</p> <p> 'template' => 'cur-block',</p> <p> ),</p> <p> );</p> <p>}</p> </div><p>а в папке с модулем нужно разместить файл с именем cur-block.tpl.php и таким содержимым:</p> <div class='cite'><p><ul></p> <p><?php</p> <p> foreach($result as $k => $v) {</p> <p> if($v["diff"] > 0) {</p> <p> $color = "#5aaf43";</p> <p> $v["diff"] = "+" . $v["diff"];</p> <p> }</p> <p> else if($v["diff"] < 0) $color = "#f00";</p> <p> else $color = "#00f";</p> <p> print "<li>" . $v["nominal"] . " " . $v["name"] . " = " .</p> <p> $v["value"] . " (<span style=\"color: " . $color . "\">" .</p> <p> $v["diff"] . "</span>)</li>";</p> <p> }</p> <p>?></p> <p></ul></p> </div><p>Теперь, чтобы переопределить вывод данных, формируемых модулем Currencies, разработчику достаточно скопировать файл cur-block.tpl.php из папки с модулем в папку с используемой темой оформления и внести в него необходимые изменения.</p> <div class='subtitle'>Интерфейс пользователя</div><p>Сейчас наш модуль имеет интерфейс администратора, умеет автоматически соединяться с удаленным сервером, получать необходимую информацию, формировать из нее HTML-код и сохранять его в кэше. Осталась самая простая для программиста и наиболее значимая для посетителя сайта часть – вывод данных на экран.</p> <p>Для решения этой задачи мы могли бы через созданную ранее функцию currencies_menu() зарегистрировать еще один путь и в качестве параметра page callback указать функцию currencies_contents(), которая получила бы необходимые данные (из кэша или от удаленного сервера) и вывела бы их на экран через функцию темизации. Но мы хотим, чтобы курсы валют выводились не отдельной страницей, а в блоке (оформленный особым образом элемент, содержащий данные) на любой заданной администратором сайта странице. Для этого необходимо создать реализацию хука hook_block() (см. листинг 4).</p> <div class='cite'><div class='subtitle'>Листинг 4</div><p>function currencies_block($op = 'list', $delta = 0,</p> <p> $edit = array()) {</p> <p> switch ($op) {</p> <p> case 'list':</p> <p> $blocks[0] = array(</p> <p> 'info' => t('Currencies block'),</p> <p> );</p> <p> return $blocks;</p> <p> case 'view':</p> <p> switch ($delta) {</p> <p> case 0:</p> <p> $block['subject'] = t('Currencies block');</p> <p> $block['content'] = currencies_contents();</p> <p> break;</p> <p> }</p> <p> return $block;</p> <p> }</p> <p>}</p> </div><p>Параметр $op (сокращение от operation), содержит информацию о том, какая операция над блоком в данный момент выполняется. Значение list сообщает модулю, что информация о нем выводится в списке модулей, здесь мы должны передать ядру Drupal один обязательный параметр – заголовок модуля и ряд необязательных параметров, информацию о которых можно найти в документации.</p> <p>Значение view аргумента $op означает, что пользователь просматривает страницу, на которой должен быть отображен наш блок, и модуль обязан вернуть его содержимое. Для этого функция должна сформировать и выдать ассоциативный массив, элемент с ключом content которого будет использован в качестве содержимого блока, а необязательный элемент с ключом subject – заголовка.</p> <p>Каждый модуль может создавать несколько блоков, параметр $delta хука hook_block содержит индекс обрабатываемого в текущий момент блока.</p> <p>Все, разработка модуля завершена! Теперь администратор сайта, после инсталляции модуля Currencies, может активировать созданный им блок, для этого необходимо перейти в меню Administer – Blocks (admin/build/block), перетащить мышкой строку с описанием блока в нужный регион (область страницы, которая может содержать один или несколько блоков) и сохранить изменения. В свойствах модуля доступны стандартные для Drupal средства управления блоком: список страниц, на которых блок должен показываться (или наоборот список страниц, на которых блок не должен показываться), список ролей пользователей, которые имеют доступ к содержимому блока, и другие.</p> <div class='cite'><div class='subtitle'>Ссылки на документацию</div><p>• Руководство по разработке модулей: http://drupal.org/node/206754.</p> <p>• Параметры, используемые в .info-файлах: http://drupal.org/node/231036.</p> <p>• Введение в Schema API: http://drupal.org/node/146843.</p> <p>• Типы полей, используемых в Schema API: http://drupal.org/node/159605.</p> <p>• Описание hook_menu(): http://api.drupal.org/api/function/hook_menu/6.</p> <p>• Описание типов полей Forms API: http://api.drupal.org/api/file/developer/topics/forms_api_reference.html/6.</p> <p>• Описание hook_block(): http://api.drupal.org/api/function/hook_block/6.</p> </div><br><div class='cite'><div class='subtitle'>Новости. С 15 по 15</div><p><b>Принтеры</b></p> <p>Компания Konica Minolta (www.konicaminolta.ru) объявила о выпуске универсального драйвера. (UPD, Universal Print Driver) для всех устройств печати Konica Minolta и других производителей, совместимых с PCL6 или PostScript. Пакет будет полезен компаниям с большим парком разнообразного оборудования; система автоматически распознает доступные сетевые устройства печати и отображает их в виде списка, из которого пользователь выбирает подходящие. С помощью драйвера администраторы могут дистанционно настраивать многофункциональные аппараты и принтеры.</p> <p>Универсальный драйвер печати имеет унифицированный графический интерфейс, предоставляет доступ ко всем возможностям устройства, от выбора формата до финишной обработки и настройки качества печати. Кроме того, все основные настройки, например формат бумаги и тип печати, могут быть одинаково применены на всех устройствах без установки дополнительных драйверов.</p> <p>Предусматривается фильтр поиска, позволяющий искать печатное устройство по заданным параметрам. При необходимости печатать высококачественные цветные отпечатки формата А3 в дуплексном режиме пользователь просто выбирает соответствующие характеристики, такие как цвет, поддержка формата А3 и двусторонняя печать в поисковом окне. В окне фильтра появится список печатных устройств, отвечающих задаче.</p> <p><b>Программы</b></p> <p>Компания Entensys (www.usergate.ru) объявила о выпуске UserGate Proxy & Firewall 5.2. Система предназначается для организации доступа в Интернет и защиты локальной сети от внешних угроз, одно из главных нововведений – обновленный механизм блокирования HTTP-трафика по заголовку Content-Type, что позволяет блокировать определенные типы данных (картинки, видеоданные, флэш-контент и др.). Изменения в области информационной безопасности коснулись соединения между консолью и сервером – в новой версии используется SSL-защита всех передаваемых данных между этими модулями. Реализован мастер настройки, при помощи пошаговых инструкций эта функция позволяет быстро настроить сервер. Изменился и раздел статистики, появились новые отчеты («пользователь – сайты» и «группа пользователей – сайты»). Отметим также усовершенствованный механизм конвертации конфигурационного файла из форматов предыдущих версий UserGate, автоматическое восстановление базы данных в случае повреждения и оптимизацию работы модуля кэширования. Зарегистрированные пользователи версии 5.х могут получить обновление бесплатно.</p> <p><b>Программы</b></p> <p>Выпущена новая версия пакета Hidden Administrator (www.hidadmin.ru). Программа предназначена для удаленного управления компьютерами по сети и через Интернет. Среди ее возможностей – полный доступ к ресурсам удаленного компьютера, скрытое наблюдение за ними, управление, одновременное наблюдение за множеством компьютеров (до 256), запись изображения с удаленного экрана в видеофайл формата AVI и др.</p> </div></section></section> <div id="adfox_164786071391256813"></div> <script> window.yaContextCb.push(() => { Ya.adfoxCode.createAdaptive({ ownerId: 332443, containerId: 'adfox_164786071391256813', params: { p1: 'ctdwx', p2: 'gxmy' } }, ['desktop', 'tablet', 'phone'], { tabletWidth: 830, phoneWidth: 480, isAutoReloads: false }) }) </script> <div class="pagination"> <!-- if($content->bookInfo->litres_url == "" --> <a href="https://fb2.top/ghurnal-pc-magazine-re-09-2009-171245/read/part-8" class="btn btn-outline-dark btn-block btn-lg mr-1">< Назад</a> <a href="#" class="btn btn-outline-dark btn-block btn-lg mx-1 mt-0" data-toggle="modal" data-target="#modalContents"><i class="fas fa-list-ul"></i></a> <a href="https://fb2.top/ghurnal-pc-magazine-re-09-2009-171245/read/part-10" class="btn btn-outline-dark btn-block btn-lg mt-0 ml-1">Далее ></a> <!-- // if($content->bookInfo->litres_url == "" --> </div> </div> <div class="d-none d-lg-block col-lg-4 col-xl-4"> <div class="sidebar"> <div class="sidebar-inner"> <div class="sidebar-box tg mt-3 mt-lg-0 "> <div class="sidebar-content"> <div class="sidebar-title"><img src="/img/tg_logo_32.png"> ТЕЛЕГРАМ</div> <p>Канал с обзорами, анонсами новинок и книжными подборками</p> <a class="tg-btn" rel="nofollow" target="_blank" href="https://t.me/duosoft_books" onclick="ym(67247512,'reachGoal','5');"><img src="/img/vestnik.knig-32.jpg"> Книжный Вестник</a> <div class="grey-line mb-3"></div> <p>Бот для удобного поиска книг (если не нашлось на сайте)</p> <a class="tg-btn" rel="nofollow" target="_blank" href="https://t.me/fb2top_bot" onclick="ym(67247512,'reachGoal','6');"><img src="/img/bot-32.jpg"> Поиск книг</a> <div class="grey-line mb-3"></div> <p>Свежие любовные романы в удобных форматах</p> <a class="tg-btn" rel="nofollow" target="_blank" href="https://t.me/newlovebooks" onclick="ym(67247512,'reachGoal','7');"><img src="/img/newlovebooks-32.jpg"> Любовные романы</a> <div class="grey-line mb-3"></div> <p>О психологии, саморазвитии и личностном росте</p> <a class="tg-btn" rel="nofollow" target="_blank" href="https://t.me/hotpsychologybooks " onclick="ym(67247512,'reachGoal','21');"><img src="/img/hotpsychologybooks-32.jpg"> Саморазвитие</a> <div class="grey-line mb-3"></div> <p>Детективы и триллеры, все новинки</p> <a class="tg-btn" rel="nofollow" target="_blank" href="https://t.me/hotdetectivebooks" onclick="ym(67247512,'reachGoal','14');"><img src="/img/DETECTIVE-32.jpg"> Детективы</a> <div class="grey-line mb-3"></div> <p>Фантастика и фэнтези, все новинки </p> <a class="tg-btn" rel="nofollow" target="_blank" href="https://t.me/hotfantasticbooks" onclick="ym(67247512,'reachGoal','15');"><img src="/img/fantasy-32.jpg"> Фантастика</a> <div class="grey-line mb-3"></div> <p>Отборные классические книги </p> <a class="tg-btn" rel="nofollow" target="_blank" href="https://t.me/freeclassicbooks" onclick="ym(67247512,'reachGoal','16');"><img src="/img/classicbooks-32.jpg"> Классика</a> </div> </div> <div class="sidebar-box vk mt-3"> <div class="sidebar-content"> <div class="sidebar-title"><img src="/img/vk.png"> ВКОНТАКТЕ</div> <p>Цитаты, афоризмы, стихи, книжные подборки, обсуждения и многое другое</p> <a class="vk-btn" rel="nofollow" target="_blank" href="https://vk.com/duosoft_books" onclick="ym(67247512,'reachGoal','9');"><img src="/img/vestnik.knig-32.jpg"> Книжный Вестник</a> </div> </div> <div class="sidebar-box insta mt-3"> <div class="sidebar-content"> <div class="sidebar-title"> БИБЛИОТЕКИ</div> <p>Библиотека с любовными романами, которая наверняка придётся по вкусу женской части аудитории</p> <a class="insta-btn" target="_blank" href="https://ladylib.top"><img src="/img/newlovebooks-32.jpg"> Любовные романы</a> <div class="grey-line mb-3"></div> <p>Библиотека с фантастикой и фэнтези, а также смежных жанров</p> <a class="insta-btn" target="_blank" href="https://fictionbooks.top"><img src="/img/fantasy-32.jpg"> Фантастика</a> <div class="grey-line mb-3"></div> <p>Самые популярные книги в формате фб2</p> <a class="insta-btn" target="_blank" href="https://фб2.рф"><img src="/img/classicbooks-32.jpg"> Топ фб2 книги</a> </div> </div> </div> </div> </div> </div> </div> <div class="modal fade" id="modalContents" tabindex="-1" aria-labelledby="modalContentsTitle" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable"> <div class="modal-content"> <div class="modal-header"> <h3 class="modal-title p-0">Оглавление</h3> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <a rel='nofollow' href='/abuse?bookId=171245'><i class='fas fa-exclamation-circle'></i> Пожаловаться</a><ul class='pl-2'><li class='mg-0 mt-3 mb-3'><a href='https://fb2.top/ghurnal-pc-magazine-re-09-2009-171245'>К описанию</a></li><li class=' mt-1 mg-1'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-1#1'>Новости Новости и комментарии: pcmag.ru/news</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-1#2'>…плюс викификация вооруженных сил</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-1#3'>Австралия – земля несвободы</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-1#4'>Техасское правосудие: Microsoft – 2:0</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-1#5'>Интернет-няня с отечественным лицом</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-1#6'>Тайное становится явным</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-1#7'>Время закрывать Wintel?</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-1#8'>Беззащитная клавиатура</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-1#9'>Интернет как сон суперпингвина</a></li><li class=' mt-1 mg-1'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-2#10'>Актуальные новинки</a></li><li class=' mt-1 mg-1'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-3#11'>На первый взгляд Новинки индустрии: pcmag.ru/guide/</a></li><li class=' mt-1 mg-1'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-4#12'>Лаборатория Тесты и обзоры: pcmag.ru/reviews</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-4#13'>Программы</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-4#14'>Проекторы</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-4#15'>Сканеры</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-4#16'>Электронные книги</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-4#17'>Системные платы</a></li><li class=' mt-1 mg-1'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-5#18'>Гид покупателя</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-5#19'>Коммуникаторы: парад тенденций</a></li><li class=' mt-1 mg-1'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-6#20'>Инфраструктура</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-6#21'>В преддверии SaaS</a></li><li class=' mt-1 mg-1'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-7#22'>Сделай сам</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-7#23'>Игры по сети WiFi: настройка маршрутизатора</a></li><li class=' mt-1 mg-1'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-8#24'>Короли, капуста и… компьютеры Мнения и мысли: pcmag.ru/columns</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-8#25'>Facebook и Twitter: битва за знаменитостей</a></li><li class='actual mt-1 mg-1'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-9#26'>Разработка ПО</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-9#27'>«Лента друзей»: разрабатываем компонент «1С-Битрикс»</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-9#28'>Drupal: разработка модуля</a></li><li class=' mt-1 mg-1'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-10#29'>Операционные системы</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-10#30'>Windows 7: новые средства управления энергопотреблением</a></li><li class=' mt-1 mg-1'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-11#31'>Проблемы и решения Советы и секреты: pcmag.ru/solutions/</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-11#32'>Mac под управлением Windows</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-11#33'>Социальные сети: осторожно, Facebook!</a></li><li class=' mt-1 mg-2'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-11#34'>Мгновенные сообщения как офисный инструмент</a></li><li class=' mt-1 mg-1'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-12#35'>Читайте в следующих номерах</a></li><li class=' mt-1 mg-0'><a href='/ghurnal-pc-magazine-re-09-2009-171245/read/part-notes#0'>Примечания</a></li></ul> </div> </div> </div> </div> <div class="modal fade" id="modalNote" tabindex="-1" role="dialog" aria-labelledby="modalNotesTitle" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered"> <div class="modal-content"> <div class="modal-body"> </div> <div class="modal-footer"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">закрыть</span> </button> </div> </div> </div> </div> <div id="reader_nodes" class="d-none"> </div> <div id="loader-fullscreen"> <div class="d-flex justify-content-center align-items-center"> <div class="spinner-border" role="status"> <span class="sr-only">Загрузка...</span> </div> </div> </div> <input name="bookId" type="hidden" value="171245"> <input name="from_cache" type="hidden" value="0"> </div> <footer id="footer"> <div class="container menu-row"> <div> <a id="btn-zoom-plus" class="pl-1" href="#"><i class="fas fa-search-plus"></i></a> <a id="btn-zoom-minus" class="pl-3" href="#"><i class="fas fa-search-minus"></i></a> <a id="btn-moon" class="pl-3" href="#"><i class="far fa-moon"></i></a> <a id="btn-sun" class="pl-3" href="#"><i class="fas fa-sun "></i></a> </div> <div> </div> <div> <a class="tg btn px-1 d-inline d-lg-none" rel="nofollow" target="_blank" href="https://vk.com/duosoft_books" onclick="ym(67247512,'reachGoal','9');"><img src="/img/vk.png" class="img-fluid" alt="Наш паблик в ВК" title="Наш паблик в ВК"></a> <a class="tg btn px-1 d-inline" rel="nofollow" target="_blank" href="https://t.me/duosoft_books" onclick="ym(67247512,'reachGoal','5');"><img src="/img/tg_logo_32.png" class="img-fluid" alt="Наш Телеграм канал" title="Наш телеграм канал"><span class="d-none d-md-inline"> Наш канал</span></a> </div> </div> </footer> <!-- Styles --> <link rel="stylesheet preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" as="style" type="text/css"/> <!-- Scripts --> <script src="/lib/jquery-3.6.0.min.js" defer></script> <script src="/lib/bootstrap-4.6.0/js/bootstrap.min.js" defer></script> <script src="/lib/lib.js?v=2" defer></script> <script src="/js/model/settings.js" defer></script> <script src="/js/reader.js?v=20" defer></script> <!-- Yandex.Metrika counter --> <script type="text/javascript"> (function (m, e, t, r, i, k, a) { m[i] = m[i] || function () { (m[i].a = m[i].a || []).push(arguments) }; m[i].l = 1 * new Date(); k = e.createElement(t), a = e.getElementsByTagName(t)[0], k.async = 1, k.src = r, a.parentNode.insertBefore(k, a) }) (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); ym(67247512, "init", { clickmap: true, trackLinks: true, accurateTrackBounce: true, webvisor: true }); </script> <noscript> <div><img src="https://mc.yandex.ru/watch/67247512" style="position:absolute; left:-9999px;" alt=""/></div> </noscript> <!-- /Yandex.Metrika counter --> <!-- Global site tag (gtag.js) - Google Analytics --> <script async src="https://www.googletagmanager.com/gtag/js?id=UA-177760544-1"></script> <script> window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', 'UA-177760544-1'); </script> <div class="modal fade" id="modalSocials" tabindex="-1" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable"> <div class="modal-content"> <div class="modal-header"> <h3 class="modal-title p-0">Нравится библиотека?</h3> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <p style="text-align: center">Присоединяйтесь к нашим литературным сообществам!</p> <a class="vk-btn" rel="nofollow" target="_blank" href="https://vk.com/duosoft_books" onclick="ym(67247512,'reachGoal','17');"><img src="/img/vk.png"> ВКОНТАКТЕ</a> <a class="tg-btn" rel="nofollow" target="_blank" href="https://t.me/duosoft_books" onclick="ym(67247512,'reachGoal','19');"><img src="/img/tg_logo_32.png"> В ТЕЛЕГРАМ</a> </div> </div> </div> </div> <div id="adfox_166480280077036586"></div> <script> setTimeout(() => { let shouldReload = true; window.yaContextCb.push(() => { Ya.adfoxCode.createAdaptive({ ownerId: 332443, containerId: 'adfox_166480280077036586', onClose: function (event) { shouldReload = false; }, type: 'floorAd', params: { p1: 'cvxjf', p2: 'heya' } }, ['phone'], { tabletWidth: 830, phoneWidth: 480, isAutoReloads: false }) }); setInterval(function () { if (shouldReload) { window.Ya.adfoxCode.reload('adfox_166480280077036586', {onlyIfWasVisible: true}); } }, 30000); }, 5000) </script> <div id="adfox_16996237345594861"></div> <script> if (screen.width >= 830) { window.yaContextCb.push(() => { Ya.adfoxCode.createAdaptive({ ownerId: 332443, type: 'floorAd', platform: 'desktop', containerId: 'adfox_16996237345594861', params: { p1: 'dabiu', p2: 'iqvg' } }, ['desktop', 'tablet'], { tabletWidth: 830, phoneWidth: 480, isAutoReloads: false }) }) } </script> </body> </html>