Былина о том, как я Drupal и Яндекс.ПДД связывал

Некоторое время назад у меня была идея сделать городской сайт (естественно, очередной) для одного небольшого городка на базе Drupal. Как раз незадолго до этого Яндекс расширил функционал своей Почты для доменов и добавил возможность управлять ящиками через API. И в голове зародилась мысль:, а почему бы и нет? Почему бы не предоставить пользователям возможность одновременно с регистрацией на городском портале получать почтовый ящик в городском домене? Сама по себе идея, конечно, не удивительна, однако готовых решений не было. Сайт я так и не запустил, а коду без дела лежать грех.Как известно, Drupal позиционируется как CMS, ориентированная разработчиков и позволяющая благодаря обширной API разработать хоть систему для управления лунной базой. Я не буду вдаваться в своём посте в её восхваление и самые начальные основы разработки модулей под Drupal, однако начинающие Drupal-разработчики наверняка сочтут материал полезным, а опытным буду благодарен за советы и рекомендации.

7cdab9adbbdc4d6ea7c486fb2ac40025.png

Постановка задачиЕстественно, приступая к написанию модуля, нужно решить, каким функционалом он будет обладать. Модуль рассчитан на социальный сайт, где смогут регистрироваться пользователи. Следовательно, он должен: при регистрации пользователя предлагать ему возможность создать ящик на Яндексе; при подтверждении регистрации инициировать создание ящика; отображать авторизированному пользователю блок с информацией о количестве новых писем и возможностью перейти в ящик; создавать админ-часть для ввода необходимых параметров. Конечно, можно было бы добавить ещё какие-нибудь плюшки (например, изменять пароль к ящику при изменении пароля в нашей системе, редактировать пользовательские данные, управлять переадресацией), но это скорее излишества…info-файл Любой модуль Drupal должен содержать .info-файл со служебной информацией о модуле. Минимальный набор — название модуля, краткое описание и версия ядра, для которой он написан. Но в нашем случае необходимо указать ещё одно поле — зависимости. В логике модуля заложено создание ящиков, логин которых совпадает с логином пользователя. Но чтобы не лишать пользователей возможность создавать кириллические логины и не удивлять ими Яндекс, нам нужен модуль транслитерации. Таким образом, файлик yandex_pdd.info у нас будет выглядеть следующим образом: name = Yandex PDD description = Yandex PDD mailboxes autocreation. core = 7.x dependencies[] = transliteration Естественно, перед установкой модуля нужно не забыть установить у себя этот самый transliteration.Файл установки и удаления Ещё один обязательный файл — .install (в нашем случае — yandex_pdd.install), который определяет используемые модулем базы данных и необходимые действия при установке и удалении модуля. Для работы нам понадобится одна база данных и две переменных. Для начала определим структуру базы (хук hook_schema ()) function yandex_pdd_schema () { $schema['yandex_pdd'] = array ( 'fields' => array ( 'id' => array ('type' => 'serial', 'not null' => TRUE), // Идентификатор строки 'uid' => array ('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0,), // Идентификатор пользователя 'login' => array ('type' => 'varchar', 'length' => 100, 'not null' => TRUE), // Транслитерированный логин для создания ящика 'activated' => array ('type' => 'varchar', 'length' => 1, 'not null' => TRUE) // Флаг активации ящика ), 'primary key' => array ('id'), ); return $schema; } Первые две колонки, думаю, вопросов не вызывают. В третью колонку мы будем записывать логин самого электронного ящика. Четвёртую колонку стоит разобрать отдельно. В CMS Drupal для активации пользователя необходимо, чтобы он перешёл по ссылке, присланной ему на e-mail, и задал свой пароль. Известный факт, что часть учётных записей никогда не будет активирована (часть спам-ботов и просто странные люди), потому создавать ящик имеет смысл, когда пользователь задаёт новый пароль. Собственно, это заодно позволяет нам создать ящик с этим же паролем. Данный вопрос я ещё затрону ниже.При удалении модуля описанные в схеме базы удаляются автоматически. А вот переменные, закешированные данные и т.п. следует за собой убрать с помощью hook_uninstall ().

function yandex_pdd_uninstall () { variable_del ('yandex_pdd_domain'); variable_del ('yandex_pdd_authtoken'); cache_clear_all ('yandex_pdd','cache', TRUE); menu_rebuild (); } Параметры yandex_pdd_domain и yandex_pdd_authtoken задают домен, в котором будут создаваться ящики, и API-ключ соответственно. Расскажу о них подробнее немного позже.Необходимый фундамент для работы модуля заложен, и мы можем приступать к написанию самого кода модуля. Храниться он у нас будет, как и полагается, в файле с расширением .module (yandex_pdd.module).

Элементы меню Если модулю Drupal необходимо прописываться в меню, создавать какие-либо страницы и т.п., нам нужно описать всю структуру и необходимые действия при переходе по ссылкам с помощью хука hook_menu (). В нашем случае будет использоваться два URL-а: страница настройки модуля и переадресация в почтовый ящик. function yandex_pdd_menu (){ $items = array (); $items['admin/config/content/yandex_pdd'] = array (// Ключ массива — системный путь к странице 'title' => t ('Yandex PDD'), // страницы 'page callback' => 'main_config', // Функция, которая отвечает за генерацию страницы 'type' => MENU_NORMAL_ITEM, // Способ подвязки страницы в систему меню CMS 'access callback' => TRUE, // Определяет возможность доступа к странице ); $items['mailbox'] = array ( 'title' => 'Yandex PDD login', 'page callback' => 'mailbox_login', 'type' => MENU_CALLBACK, 'access callback' => TRUE, ); </p> <p> return $items; } Обратите внимание на использование системной функции t (). Она позволяет переводить элементы текста на текущий системный язык. И да, на странице mailbox нам такая возможность не понадобится.Страница настройки В предыдущем разделе мы рассказали нашей CMS, что страница для ввода необходимых модулю данных будет размещаться по пути admin/config/content/yandex_pdd и обрабатываться функцией main_config. Собственно, CMS на самом деле не в курсе, чем конкретно будет заниматься страница, но знает, что при обращении по данному пути нужно обратиться к данной функции. function main_config (){ $form = drupal_get_form ('pdd_config_form'); // Получаем поля формы $form = drupal_render ($form); // Рендерим форму return $form; } Для создания страницы мы получаем друпалопонятный массив полей формы обращением системной функции drupal_get_form () к пользовательской функции pdd_config_form. function pdd_config_form ($form, &$form_state){ $form=array (); $form['pdd_domain'] = array (// Ключ массива — имя поля в форме '#type' => 'textfield', // Тип поля '#title' => t ('Domain zone'), // Название поля, понятное пользователю '#description' => t ('A domain zone in which email should be created.'), // Описание поля '#default_value' => variable_get ('yandex_pdd_domain'), // Значение по умолчанию '#required' => 1, // Флаг обязательности ); $form['authtoken'] = array ( '#type' => 'textfield', '#title' => t ('Auth Token'), '#description' => t ('Authorization token obtained at Yandex.PDD service.'), '#default_value' => variable_get ('yandex_pdd_authtoken'), '#required' => 1, ); $form['submit'] = array ( '#type' => 'submit', '#value' => t ('Submit'), ); return $form; } Результат передаётся функции drupal_render (), которая собирает массив в готовый html-код страницы.Форма содержит всего два поля: </p><p>pdd_domain — подвязанный к Яндекс.ПДД домен, в котором будут создаваться ящики; authtoken — авторизационный токен, полученный в настройках API Почты для домена. <img src="http://habrastorage.org/files/761/f67/678/761f67678913429da27fb0b6bf6fd6ff.png" alt="761f67678913429da27fb0b6bf6fd6ff.png" />Естественно, результаты заполнения формы нам необходимо сохранить. За это отвечает функция pdd_config_form_submit, которая просто сохраняет значения полей в системные переменные.</p><p> function pdd_config_form_submit ($form, &$form_state){ variable_set ('yandex_pdd_domain', $form_state['values']['pdd_domain']); variable_set ('yandex_pdd_authtoken', $form_state['values']['authtoken']); } Ещё небольшой кусочек кода — реализация хука hook_help (), который выводит справочную информацию о модуле в соответствующем разделе админ-части. По поводу этой функции я не заморачивался и пошёл по пути минимализма. function yandex_pdd_help ($path, $arg) { switch ($path) { case «admin/help#yandex_pdd»: return '<p>'. t («Yandex PDD mailboxes management module.») .'</p>'; break; } } С административной частью разобрались. Пора заняться пользовательской, весь функционал которой можно разделить на этап создания и этап работы.Создание ящика Не всем нашим пользователям может понадобиться ещё один e-mail, пусть даже и в домене любимого сайта. Да и нам ни к чему плодить огромное количество неиспользуемых ящиков. Потому мы добавим в форму регистрации пользователя чекбокс, который позволит ему выбирать, создавать ящик или нет. Любую форму в Drupal можно изменить с помощью hook_form_alter (). С назначением некоторых элементов массива описания формы мы уже знакомы. Подробнее со всеми остальными можно познакомиться на справочной странице по формам. function yandex_pdd_form_alter (&$form, &$form_state, $form_id){ if ($form_id=='user_register_form'){ // Идентификатор изменяемой формы $form['account']['createmail']=array (// Имя дополнительного поля '#type' => 'checkbox', '#title' => t ('Create a mailbox'), '#description' => t ('Check this box if you want to create a mailbox @'.variable_get ('yandex_pdd_domain').'.'), '#required' => 0, '#access' => 1, // Доступно ли поле пользователям '#weight' => 10 // Вес (критерий сортировки) ); } } Много текста для одного маленького чекбокса.<img src="http://habrastorage.org/files/96d/c36/d61/96dc36d61fc24dbb85da9576b4c06b1a.png" alt="96dc36d61fc24dbb85da9576b4c06b1a.png" />При регистрации пользователя мы проверяем, отмечен ли чекбокс создания ящика в форме, и если да, вносим в таблицу данные о неактивном ящике. function yandex_pdd_user_insert (&$edit, $account, $category){ if ($account→createmail){ $transliterated = transliteration_get ($account→name, '_'); // Транслитерируем логин $pattern = '/[^a-zA-Z0–9]/'; // Задаём шаблон для замены всего, кроме букв и цифер $transliterated = preg_replace ($pattern, '-', $transliterated); // Заменяем все не alphanumeric знаки на дефисы $newbox = db_insert ('yandex_pdd'); // Инициируем вставку в базу данных $newbox→fields (array ('uid' => $account→uid, 'login' => strtolower ($transliterated), 'activated' => '0')); // Задаём данные для вставки $res = $newbox→execute (); // Выполняем запрос watchdog ('yandex_pdd', print_r ('Res: '.$res,1)); // Для отладки записываем в системный журнал CMS результат } } Создание ящика подвязано к изменению формы с пользовательскими данными хуком hook_field_attach_submit (). Таким образом, до того, как пользователь впервые произведёт редактирование данных (изменение пароля), ящик не создаётся. function yandex_pdd_field_attach_submit ($entity_type, $entity, $form, &$form_state) { global $user; if ($entity_type == 'user' and $user→uid > 0) { // Обработка при изменении сущности типа user $select = db_select ('yandex_pdd','ypdd'); $select→addField ('ypdd', 'id'); $select→addField ('ypdd', 'login'); $select→addField ('ypdd', 'activated'); $select→condition ('uid', $user→uid); $entries = $select→execute ()→fetchAssoc (); // Получаем логин для ящика и статус активации if (array_key_exists ('login', $entries) and $entries['login'] != '' and $entries['activated'] == 0) { // Если пользователю нужен ящик и он не создан $mailboxcreate = simplexml_load_file ('https://pddimp.yandex.ru/reg_user_token.xml? token='.variable_get ('yandex_pdd_authtoken').'&u_login='.$entries['login'].'&u_password='.$form[»#user»]→pass); // Создаём ящик и парсим XML-ответ if ($mailboxcreate→ok[0]) { // Если создался $num_updated = db_update ('yandex_pdd'); $num_updated→fields (array ('activated' => '1')); $num_updated→condition ('uid', $user→uid); $res = $num_updated→execute (); // Отмечаем флаг активации ящика } elseif ($mailboxcreate→error[0]) { // Если ошибка API foreach ($mailboxcreate→error[0]→attributes () as $key => $value) { $mbc[$key] = (string)$value; } watchdog ('yandex_pdd', «Can’t create new mailbox. Reason:».$mbc['reason']); // Записываем в лог сообщение } else { // Если вообще неясно что случилось watchdog ('yandex_pdd','Unknown error while creating mailbox.'); // Так и запишем } } } } Наконец-то наш пользователь получил ящик, хотя не особо знает, как туда попасть. Осталось реализовать функционал перехода на почту.Рабочий функционал Собственно, пользовательских функций всего две: отобразить блок и авторизироваться на почте. Блок нужно в первую очередь описать хуком hook_block_info () function yandex_pdd_block_info () { $blocks['mailbox_status'] = array (// Системное имя блока 'info' => t ('Mailbox status'), // Административное название блока 'cache' => DRUPAL_CACHE_PER_ROLE, // Режим кеширования ); return $blocks; } Для блока нам ещё понадобится задать темизацию и файл с шаблоном оформления. Темизация описывается хуком hook_theme (), описывающим системе реализацию оформления элементов модуля. function yandex_pdd_theme () { return array ( 'yandex_pdd_block' => array (// Системное имя шаблона 'variables' => array (// Используемые шаблоном переменные 'newmail' => NULL ), 'template' => 'yandex-pdd-block', // Имя файла шаблона ) ); } И, собственно, сам файл yandex-pdd-block.tpl.php. <div class="yandexpdd"><?php print t('You have ').'<a href="/mailbox" target="_blank">'.$newmail.t (' new messages').'</a>.'; ?></div> Для блока нам нужно получить по API одно-единственное значение — количество новых писем, после чего отрендерить сам блок хуком hook_block_view () function yandex_pdd_block_view ($delta = '') { global $user; if ($user→uid > 0) { // Проверяем, авторизирован ли пользователь $select = db_select ('yandex_pdd','ypdd'); $select→addField ('ypdd', 'login'); $select→condition ('uid', $user→uid); $entries = $select→execute ()→fetchAssoc (); // Получаем логин ящика $unreadmailxml = simplexml_load_file ('https://pddimp.yandex.ru/get_mail_info.xml? token='.variable_get ('yandex_pdd_authtoken').'&login='.$entries['login']); // Получаем от Яндекса информацию о ящике if ($unreadmailxml→ok[0]) { // Если запрос успешен foreach ($unreadmailxml→ok[0]→attributes () as $key => $value) { // Парсим ответ $unreadmail[$key] = (string)$value; } $blocks = array (); $blocks['subject'] = null; $blocks['content'] = theme ('yandex_pdd_block', array ('newmail' => $unreadmail['new_messages'])); return $blocks; // Возвращаем блок } elseif ($unreadmailxml→error[0]) { foreach ($unreadmailxml→error[0]→attributes () as $key => $value) { $unreadmail[$key] = (string)$value; } watchdog ('yandex_pdd', «Can’t get new mail info. Reason:».$unreadmail['reason']); } else { watchdog ('yandex_pdd','Unknown error while loading new mail info'); } } } Сгенерированный блок будет выглядеть примерно так.<img src="http://habrastorage.org/files/b55/ba7/3af/b55ba73aff844f5187572a755ee861c7.png" alt="b55ba73aff844f5187572a755ee861c7.png" /></p> <p>Завершающий штрих — системная ссылка mailbox, которая будет перебрасывать пользователя на страницу Яндекс.Почты и авторизировать по одноразовому токену. Как мы помним (или уже не помним), мы ранее задали, что эта ссылка должна обрабатываться функцией mailbox_login. Время жизни токена всего 30 секунд, потому после его получения пользователь тут же должен быть переадресован на страницу авторизации.</p><p> function mailbox_login (){ global $user; global $base_url; if ($user→uid > 0) { $select = db_select ('yandex_pdd','ypdd'); $select→addField ('ypdd', 'login'); $select→condition ('uid', $user→uid); $entries = $select→execute ()→fetchAssoc (); $tokenxml = simplexml_load_file ('https://pddimp.yandex.ru/api/user_oauth_token.xml? token='.variable_get ('yandex_pdd_authtoken').'&domain='.variable_get ('yandex_pdd_domain').'&login='.$entries['login']); // Получаем одноразовый токен авторизации if ($tokenxml→xpath ('status/success')) { $tokenarr = $tokenxml→xpath ('domains/domain/email/oauth-token'); header ('Location: http://passport.yandex.ru/passport? mode=oauth&type=trusted-pdd-partner&error_retpath='.urlencode ($base_url.'/').'&access_token='.(string)$tokenarr[0]); // Перебрасываем пользователя на Яндекс.Почту drupal_exit (); // Прекращаем любые другие действия CMS } elseif ($tokenxml→xpath ('status/error')) { watchdog ('yandex_pdd', «Can’t get short-term auth token info. Reason:».(string)$tokenxml→xpath ('action/status/error')); } } } Заключение Тут и сказу конец. Размещённые куски кода, собранные в соответствии с указанными именами файлов, должны дать Вам готовый модуль. Надеюсь, кому-то он будет полезен, и с удовольствием выслушаю замечания и рекомендации по коду.</p> <p class="copyrights"><span class="source">© <a target="_blank" rel="nofollow" href="http://habrahabr.ru/post/245117/">Habrahabr.ru</a></span></p> </div> <br> <!--<div align="left"> <script type="text/topadvert"> load_event: page_load feed_id: 12105 pattern_id: 8187 tech_model: </script><script type="text/javascript" charset="utf-8" defer="defer" async="async" src="//loader.topadvert.ru/load.js"></script> </div> <br>--> <div style="padding-left: 20px;"> <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2514821055276660" crossorigin="anonymous"></script> <!-- PCNews 336x280 --> <ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-2514821055276660" data-ad-slot="1200562049" data-ad-format="auto"></ins> <script> (adsbygoogle = window.adsbygoogle || []).push({}); </script> </div> <!-- comments --> <noindex> <div style="margin: 25px;" id="disqus_thread"></div> <script type="text/javascript"> var disqus_shortname = 'pcnewsru'; var disqus_identifier = '589465'; var disqus_title = 'Былина о том, как я Drupal и Яндекс.ПДД связывал'; var disqus_url = 'http://pcnews.ru/blogs/bylina_o_tom_kak_a_drupal_i_andekspdd_svazyval-589465.html'; (function() { var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true; dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js'; (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq); })(); </script> <!--<noscript>Please enable JavaScript to view the <a rel="nofollow" href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>--> <!--<a href="http://disqus.com" rel="nofollow" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>--> </noindex> </div> <br class="clearer"/> </div> <br class="clearer"/> <div id="footer-2nd"></div> <div id="footer"> <br/><br/> <ul class="horz-menu"> <li class="about"><a href="/info/about.html" title="О проекте">О проекте</a></li> <li class="additional-menu"><a href="/archive.html" title="Архив материалов">Архив</a> </li> <li class="additional-menu"><a href="/info/reklama.html" title="Реклама" class="menu-item"><strong>Реклама</strong></a> <a href="/info/partners.html" title="Партнёры" class="menu-item">Партнёры</a> <a href="/info/legal.html" title="Правовая информация" class="menu-item">Правовая информация</a> <a href="/info/contacts.html" title="Контакты" class="menu-item">Контакты</a> <a href="/feedback.html" title="Обратная связь" class="menu-item">Обратная связь</a></li> <li class="email"><a href="mailto:pcnews@pcnews.ru" title="Пишите нам на pcnews@pcnews.ru"><img src="/media/i/email.gif" alt="e-mail"/></a></li> <li style="visibility: hidden"> <noindex> <!-- Rating@Mail.ru counter --> <script type="text/javascript"> var _tmr = window._tmr || (window._tmr = []); _tmr.push({id: "93125", type: "pageView", start: (new Date()).getTime()}); (function (d, w, id) { if (d.getElementById(id)) return; var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id; ts.src = (d.location.protocol == "https:" ? "https:" : "http:") + "//top-fwz1.mail.ru/js/code.js"; var f = function () { var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s); }; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "topmailru-code"); </script> <noscript> <div style="position:absolute;left:-10000px;"> <img src="//top-fwz1.mail.ru/counter?id=93125;js=na" style="border:0;" height="1" width="1" alt="Рейтинг@Mail.ru"/> </div> </noscript> <!-- //Rating@Mail.ru counter --> </noindex> </li> </ul> </div> <!--[if lte IE 7]> <iframe id="popup-iframe" frameborder="0" scrolling="no"></iframe> <![endif]--> <!--<div id="robot-image"><img class="rbimg" src="i/robot-img.png" alt="" width="182" height="305" /></div>--> <!--[if IE 6]> <script>DD_belatedPNG.fix('#robot-image, .rbimg');</script><![endif]--> </div> <!--[if lte IE 7]> <iframe id="ie-popup-iframe" frameborder="0" scrolling="no"></iframe> <![endif]--> <div id="footer-adlinks"></div> <noindex> <!--LiveInternet counter--><script type="text/javascript"> document.write("<a rel='nofollow' href='//www.liveinternet.ru/click' "+ "target=_blank><img src='//counter.yadro.ru/hit?t45.6;r"+ escape(document.referrer)+((typeof(screen)=="undefined")?"": ";s"+screen.width+"*"+screen.height+"*"+(screen.colorDepth? screen.colorDepth:screen.pixelDepth))+";u"+escape(document.URL)+ ";"+Math.random()+ "' alt='' title='LiveInternet' "+ "border='0' width='1' height='1'><\/a>") </script><!--/LiveInternet--> <!-- Rating@Mail.ru counter --> <script type="text/javascript"> var _tmr = window._tmr || (window._tmr = []); _tmr.push({id: "93125", type: "pageView", start: (new Date()).getTime()}); (function (d, w, id) { if (d.getElementById(id)) return; var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id; ts.src = "https://top-fwz1.mail.ru/js/code.js"; var f = function () {var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s);}; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "topmailru-code"); </script><noscript><div> <img src="https://top-fwz1.mail.ru/counter?id=93125;js=na" style="border:0;position:absolute;left:-9999px;" alt="Top.Mail.Ru" /> </div></noscript> <!-- //Rating@Mail.ru counter --> <!-- Yandex.Metrika counter --> <script type="text/javascript"> (function (d, w, c) { (w[c] = w[c] || []).push(function () { try { w.yaCounter23235610 = new Ya.Metrika({ id: 23235610, clickmap: true, trackLinks: true, accurateTrackBounce: true, webvisor: true, trackHash: true }); } catch (e) { } }); var n = d.getElementsByTagName("script")[0], s = d.createElement("script"), f = function () { n.parentNode.insertBefore(s, n); }; s.type = "text/javascript"; s.async = true; s.src = "https://mc.yandex.ru/metrika/watch.js"; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "yandex_metrika_callbacks"); </script> <noscript> <div><img src="https://mc.yandex.ru/watch/23235610" style="position:absolute; left:-9999px;" alt=""/> </div> </noscript> <!-- /Yandex.Metrika counter --> <!-- Default Statcounter code for PCNews.ru http://pcnews.ru--> <script type="text/javascript"> var sc_project=9446204; var sc_invisible=1; var sc_security="14d6509a"; </script> <script type="text/javascript" src="https://www.statcounter.com/counter/counter.js" async></script> <!-- End of Statcounter Code --> <script> (function (i, s, o, g, r, a, m) { i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments) }, i[r].l = 1 * new Date(); a = s.createElement(o), m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m) })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); ga('create', 'UA-46280051-1', 'pcnews.ru'); ga('send', 'pageview'); </script> <script async="async" src="/assets/uptolike.js?pid=49295"></script> </noindex> <!--<div id="AdwolfBanner40x200_842695" ></div>--> <!--AdWolf Asynchronous Code Start --> <script type="text/javascript" src="https://pcnews.ru/js/blockAdblock.js"></script> <script type="text/javascript" src="/assets/jquery.min.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.json.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.form.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.easing.1.2.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/jquery/effects.core.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/browser-sniff.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/scripts.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-utils.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-auth.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-fiximg.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-infobox.js"></script> </body> </html>