Только для читателей Lifeexample возможно открыть интернет-магазин на Moguta.CMS со скидкой в 15%

<<< Простой чат на PHP || Пространство имен PHP (Внедрение в проект) >>>

Интеграция PayPal, простой PHP класс

18.12.2013
Интеграция PayPal. Простой PHP класс

Здравствуйте, уважаемые читатели блога LifeExample, в данной статье речь пойдет об интеграции мировой платежной системы PayPal с сайтом работающим на PHP. Этой осенью PayPal сообщили о начале работы с рублевыми переводами, а это значит что Россияне теперь смогут пользоваться самой надежной в мире системой электронных оплат.

PayPal (англ. «приятель, помогающий расплатиться») — крупнейший оператор электронных денежных средств. Позволяет клиентам оплачивать счета и покупки, отправлять и принимать денежные переводы. С октября 2002 года является подразделением компании eBay. По состоянию на 2012 год PayPal работает в 193 странах (хотя не во всех предоставляется полный набор услуг), имеет более 137 млн зарегистрированных пользователей, работает с 26 национальными валютами.

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

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

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

Интеграция PayPal

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

Мое решение разделяет процесс на пять объектов:

  • payPalController — этот класс занимается обработкой логики не связанных с IPN частей PayPal процессов. Они занимается проверкой двойных транзакций, отправляемых PayPal, и проверяет факт совершения IPN транзакции именно по продукту с моего сайта. В таком случае, покупка добавляется в базу данных, а покупатель получает email.
  • payPalIpn – место проведения обработки IPN. Очень важно!
  • rmwStoreDB – с помощью этого класса транзакции добавляются в базу данных с моей стороны.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!doctype html public '-//W3C//DTD HTML 4.01//EN'  'http://www.w3.org/TR/html4/strict.dtd'>
<html>
<head>
<title>Some Title</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<meta name="Generator" content="Alleycode HTML Editor">
<meta name="Description" content="Working Code">
<meta name="Keywords" content="Working Code">
</head>

<body>
<?php

   require_once ("../phpPayPalController.php");
   $ppc = new payPalController();
   $ppc->setTesting(false);
   $ppc->setLogging(false);
   $ppc->processPayPalIpnPayment();
   ?>

  </body>

Как вы видите payPalController — единственный класс, о котором нужно знать странице. Контроллер настроит остальные классы самостоятельно:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
<?php
require_once("phpPayPalIpnClass.php");
require_once("phpLoggerClass.php");
require_once("phpRmwStoreDbActions.php");
require_once("phpMyMailClass.php");


class payPalController {
   private $db;
   private $logger;
   private $testing;


   function __construct() {
      $this->logger = new myLogger();
      $this->logger->setLogFile("logger.txt");
      $this->logger->setLogging(false);
      $this->db = new rmwStoreDB();
      $this->db->setLogger($this->logger);
      $this->testing = false;
      }


   function setTesting($state) {
      $this->testing = $state;
      $this->logger->setLogging($state);
      }


   function setLogging($state) { $this->logger->setLogging($state); }


   function processPayPalIpnPayment() {
      $processor = new payPalIpn();
      $processor->setTesting($this->testing);
      $processor->setLogger($this->logger);
      $this->logger->log("Processing a payment.\r\n");

      if (!$processor->processPost()) return;
      if ($this->duplicateTransaction()) return;
      if (!$this->verify()) return;
      if (!$this->addOrderToDatabase()) return;
      $this->sendProduct();
      }


   private function duplicateTransaction() {
      $ret = false;
      if ($this->db->itemExists("orders", "payPalTransId",
                  $_POST['txn_id'])) {
         $this->logger->log("Transaction: " . $_POST['txn_id']
                     . " exists\r\n");
         $ret = true;
         }

      else {
         $this->logger->log("Transaction: " . $_POST['txn_id'] .
                     " does not exist\r\n");
         }
      return $ret;
      }


   private function verify() {
 
      if (!$this->db->itemExists("products", "id", $_POST
                  ['item_number'])) {
         $this->logger->log("Item number: " . $_POST['item_number']
                     . " doesn't exist in database\r\n");
         return false;
         }
      else {
         $this->logger->log("Item number: " . $_POST['item_number'] .
                     " exists in database\r\n");
         }
   
      $this->dbPrice = $this->db->getCellValue("price",
                  "products", "id", $_POST['item_number']);
      if ($_POST['mc_gross'] < $this->dbPrice) {
         $this->logger->log("Payment received (" . $_POST
                     ['mc_gross'] . ") less than item price. ("
                     . $this->dbPrice . "\r\n");
         return false;
         }
      else {
         $this->logger->log("Adequate payment received (" . $_POST
                     ['mc_gross'] . ").\r\n");
         }

      if ($_POST['mc_currency'] != "USD") {
         $this->logger->log("Paid in non-US funds - need to investigate.\r\n");
         return false;
         }

      else {
         $this->logger->log("US Currency received - OK.\r\n");
         }

      if ($_POST['receiver_email'] !=
                  "emailAddress@someplace.com"
                  && $_POST['receiver_email'] !=
                  "sandboxEmailAddress@someplace.com") {
         $this->logger->log("Incorrect receiver email received ("
                     . $_POST['receiver_email'] . ")\r\n");
         return false;
         }
      else {
         $this->logger->log("Correct email received (
                     "
. $_POST['receiver_email'] . ")\r\n");
         }

 
      if ($_POST['payment_status'] != "Completed") {
         $this->logger->log("Payment incomplete from PayPal\r\n");
         return false;
         }
      return true;
      }

   private function addOrderToDatabase() {
 
      $this->logger->log("Updating database.\r\n");
      $this->db->addOrUpdateUser();
      $this->db->addOrder();
      return true;
      }


   private function sendProduct() {
      $mailHandler = new myMailer();
      $mailHandler->setLogger($this->logger);

      if ($this->testing) {
         $mailTo = 'emailAddress@someplace.com';
         }
      else {
         $mailTo = $_POST['payer_email'];
         }


      if ($_POST['item_number'] == "XXXX" || $_POST
                  ['item_number'] == "XXXY") {
         if ($_POST['option_selection1'] == 'EPub') {
            $this->logger->log("Sending EPub to " . $mailTo .
                        "\r\n");
            $mailHandler->sendEbook('EPub', $mailTo);
            }
         else if ($_POST['option_selection1'] == 'MOBI') {
            $this->logger->log("Sending MOBI to " . $mailTo . "\r\n");
            $mailHandler->sendEbook('MOBI', $mailTo);
            }
         else {
            $this->logger->log("SOMETHING WRONG - Not EPub or MOBI!\r\n");
            }
         }
      }

   }
?>

Теперь перейдем к классу payPalIpn. Как было сказано ранее, этот класс ответственен за обработку IPN и отправку корректных ответов на PayPal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<?php

require_once("phpLoggerClass.php");


class payPalIpn {

   private $logger;              
   private $ipnVerifiedC;
   private $testingC;

   function __construct() {
      $this->ipnVerifiedC = false;
      $this->testingC = false;
      }


   function ipnVerified() { return $this->ipnVerifiedC; }


   function setTesting($state) { $this->testingC = $state; }


   function setLogger(myLogger &$logFile) { $this->logger = $logFile; }


   function processPost() {      

      $response = 'cmd=_notify-validate';

     
      $magicQuotesFuncExists = false;
      if(function_exists('get_magic_quotes_gpc')) {
         $magicQuotesFuncExists = true;
         }

     
      $numPosts = 0;

      foreach ($_POST as $key => $value) {
         $numPosts += 1;
         if($magicQuotesFuncExists == true && get_magic_quotes_gpc() == 1){
            $value = urlencode(stripslashes($value));
            }
         else {
            $value = urlencode($value);
            }
         $response .= "&$key=$value";
         }

     
      $header = "POST /cgi-bin/webscr HTTP/1.0\r\n";
      $header .= "Host: www.sandbox.paypal.com\r\n";
      $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
      $header .= "Content-Length: " . strlen($response) . "\r\n\r\n";

   
      if ($this->testingC) {
         $socket = fsockopen ('ssl://www.sandbox.paypal.com', 443, $socketErrNum,
                     $socketErrStr, 30);
         }
      else {
         $socket = fsockopen ('ssl://www.paypal.com', 443, $socketErrNum,
                     $socketErrStr, 30);
         }
      //Oldie:   $socket = fsockopen ('www.paypal.com', 80, $socketErrNum,
                  //$socketErrStr, 30);

      if (!$socket) {
       
         $mail_Body = "Error from fsockopen:\r\n" . $socketErrStr .
                     "\r\n\r\n" .
                     "Original PayPal Post Data (COULD BE BOGUS!)" .
                     "\r\n\r\n";
         foreach ($_POST as $key => $value) {
            $value = urlencode(stripslashes($value));
            $mail_Body .= "&$key=$value" . "\r\n";
            }
         mail($myEmail, "IPN Error Noficiation: Failed to connect to PayPal",
                     $mail_Body, "someone@somewhere.com");

       
         $this->logger->log("Socket error: " . $socketErrStr);
         return;
         }

     

      $receivedVerification = false;
      if ($numPosts > 3) {
         fputs ($socket, $header . $response);

         $this->logger->log("\r\nSENT:\r\n" . $header . $response .
                     "\r\n\r\n");

     
         $receivedVerification = false;
         while (!feof($socket)) {
            $result = fgets ($socket, 1024);  //Get a line of response
            $this->logger->log("RECEIVED: " . $result ."\r\n");
            if (strcmp ($result, "VERIFIED") == 0) $receivedVerification = true;
            }
         }
      fclose ($socket);

      $ret = false;
      if ($receivedVerification == false) {
         $this->logger->log(
            "\r\n\r\nINVALID TRANSACTION! (Improper PayPal response received)\r\n");
         }
      else {
         $this->ipnVerifiedC = true;
         $this->logger->log("TRANSACTION VERIFIED!\r\n");
         $ret = true;
         }
      return $ret;
      }

   }
   ?>

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
require_once("db_config.php");
require_once("phpLoggerClass.php");


class rmwStoreDB {

   private $loggerC;  
   private $lastRowC;

   function setlogger(mylogger $logFile) { $this->loggerC = $logFile; }


   function tellDb($sql) {
      $ret = mysql_query($sql);
      if (!$ret) {
         $this->loggerC->log(
                     "DATABASE ERROR:\r\n" . mysql_error() . "\r\n" .
                                 "Query: " . HtmlEntities($sql));
         die();
         }
      return $ret;
      }


   function itemExists($table, $column, $value) {
      $rows = $this->tellDb("Select * from " . $table . " where " .
                  $column . " = '" . $value . "'");
      $lastRowC = mysql_fetch_array($rows);
      if ($lastRowC) return true;
      return false;
      }


   function getCellValue($what, $table, $column, $theId) {
      $rows = $this->tellDb("Select " . $what . " from " . $table .
                  " where " . $column . " = " . $theId);
      $row = mysql_fetch_array($rows);
      if ($row) return $row['price'];
      else return 0.00;
      }


   function addOrUpdateUser() {
   
      $rows = $this->tellDb("Select * from customers where email = '" .
                  $_POST['payer_email'] . "'");
   
      $row = mysql_fetch_array($rows);
      if (!$row) {
         $this->loggerC->log("Adding user to database");
         $this->addUser();
         }
      else {
         $this->loggerC->log("User already exists in DB.\r\n");
       
         $this->updateUser($row);
         }
      }


   private function addUser() {
      $cmd = "Insert into customers (firstName, lastName, shippingName, email, " .
                  "addressLine1, city, state, zipCode, country) values ('"
                  . $_POST['first_name'] . "', '" .
                  $_POST['last_name'] . "', '" .
                  $_POST['address_name'] . "', '" .
                  $_POST['payer_email'] . "', '" .
                  $_POST['address_street'] . "', '" .
                  $_POST['address_city'] . "', '" .
                  $_POST['address_state'] . "', '" .
                  $_POST['address_zip'] . "', '" .
                  $_POST['address_country'] . "')";

      $this->tellDb($cmd);

      $this->loggerC->log("Added: '" . $_POST['first_name'] .
                  "', '" . $_POST['last_name'] .
                  "', '" . $_POST['address_name'] .
                  "', '" . $_POST['payer_email'] .
                  "', '" . $_POST['address_street'] .
                  "', '" . $_POST['address_city'] .
                  "', '" . $_POST['address_state'] .
                  "', '" . $_POST['address_zip'] .
                  "', '" . $_POST['address_country'] .
                  "')");
      }


   private function updateUser(array $row) {
     
      if ($row['firstName']    != $_POST['first_name']     ||
          $row['lastName']     != $_POST['last_name']      ||
          $row['shippingName'] != $_POST['address_name']   ||
          $row['email']        != $_POST['payer_email']    ||
          $row['addressLine1'] != $_POST['address_street'] ||
          $row['city']         != $_POST['address_city']   ||
          $row['state']        != $_POST['address_state']  ||
          $row['zipCode']      != $_POST['address_zip']    ||
          $row['country']      != $_POST['address_country']) {

     
         $cmd = "UPDATE customers SET ";
         $cmd .= "firstName = '"    . $_POST['first_name']      . "', ";
         $cmd .= "lastName = '"     . $_POST['last_name']       . "', ";
         $cmd .= "shippingName = '" . $_POST['address_name']    . "', ";
         $cmd .= "addressLine1 = '" . $_POST['address_street']  . "', ";
         $cmd .= "city = '"         . $_POST['address_city']    . "', ";
         $cmd .= "state = '"        . $_POST['address_state']   . "', ";
         $cmd .= "zipCode = '"      . $_POST['address_zip']     . "', ";
         $cmd .= "country = '"      . $_POST['address_country'] . "' ";
         $cmd .= "WHERE email = '" . $_POST['payer_email'] . "'";
         $this->loggerC->log("\r\nChanging user with email " . $_POST['payer_email'] . "\r\n");

         $old = $row['firstName'] . ", " . $row['lastName'] .
                     ", " . $row['shippingName'] . ", " .
                     $row['email'] . ", " .
                     $row['addressLine1'] . ", " . $row['city'] .
                     ", " .$row['state'] . ", " .
                     $row['zipCode'] . ", " . $row['country'] .
                     "\r\n\r\n";

         $this->loggerC->log($old);
         $this->loggerC->log($cmd . "\r\n");

         $this->tellDb($cmd);
         }
      }


   function addOrder() {
     
      $cmd = "Select id from customers where email = '" .
                  $_POST['payer_email'] . "'";
   
      $rows = $this->tellDb($cmd);
      $row = mysql_fetch_array($rows);
      if (!$row) {
         $this->loggerC->log("HUGE PROBLEM! CUSTOMER ID NOT FOUND -" .
                     " ABORTING\r\n");
         die();
         }
      $id = $row['id'];
      $theDate = date('F j, Y, g:i a');
      $tz = date('T');
      $ppID = $_POST['txn_id'];
      $grossPay = $_POST['payment_gross'];
      $shipping = $_POST['shipping'];
      $cmd = "Insert into orders (customer, date, timeZone, payPalTransId, " .
                  "grossPmt, shipping) values ('$id', '" .
                  "$theDate', '$tz', '$ppID', '" .
                  "$grossPay', '$shipping')";

      $this->tellDb($cmd);
      $this->loggerC->log("Inserting order into orders table:\r\n" . $cmd .
                  "\r\n\r\n");

     

      $cmd = "Select id from orders where payPalTransId = '$ppID'";
      $rows = $this->tellDb($cmd);
      $row = mysql_fetch_array($rows);
     
      if (!$row) {
         $this->loggerC->log("HUGE PROBLEM! ORDER ID NOT FOUND -" .
                     " ABORTING\r\n");
         die();
         }

      $id = $row['id'];
     
      $itemNum = $_POST['item_number'];
      $qty = $_POST['quantity'];
      $info = $_POST['option_selection1'];
      $cmd = "Insert into orderItems (orderNumber, item, quantity, extraInfo)" .
                  " values('$id', '$itemNum', '$qty'," .
                  " '$info')";
      $this->loggerC->log(
                  "Inserting into order items:\r\n" . $cmd . "\r\n");
      $this->tellDb($cmd);
     }

   }
?>

Ура! Следующие классы PHP для интеграции PayPal намного меньше предыдущих! Первый класс — почтового клиента. Понятное дело, что при его простоте можно сказать, что он должен быть более ярким и отправлять получше чем сейчас отформатированные HTML ответы на покупки. Но всему свое время.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php

require_once("emailPassword.php");
require_once("phpLoggerClass.php");
require_once("/path_to/swift_required.php");

class myMailer {

   private $myEmail;
   private $logger;  


   function __construct() {
      $this->myEmail = "someone@somewhere.com";
      }


   function setLogger(myLogger &$logFile) { $this->logger = $logFile; }


   function sendEbook($ebookType, $mailTo) {

      $this->mailWithAttachment($fileName, $path, $mailTo, $this->myEmail, $from,
                  $replyTo, $subject, $msg);
      }


   private function mailWithAttachment($filename, $path, $mailTo, $from_mail, $from_name,
               $replyto, $subject, $message) {

   
      $transport = Swift_SmtpTransport::newInstance('mailServer.com', 465, 'ssl')
         ->setUsername(hiddenEmailAccount())
         ->setPassword(hiddenEmailPassword())
         ;
      $mailer = Swift_Mailer::newInstance($transport);
      $message = Swift_Message::newInstance()
         ->setSubject($subject)
         ->setFrom(array($from_mail => $from_name))
         ->setTo(array($mailTo))
         ->setBody($message)
         //->addPart('<p>Here is the message itself</p>', 'text/html')
         ->attach(Swift_Attachment::fromPath($path.$filename))
         ;
      $this->logger->forceLog("Sending " . $filename . " to " . $mailTo .
                  " : ");
      $result = $mailer->send($message);
      $this->logger->forceLog("Result = " . $result . "\r\n");
      }
   }
?>

Последним идет класс логов:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

class myLogger {

   private $fileNameC;
   private $doLoggingC;

   function __construct() {
      $this->fileNameC = "log.txt";
      $this->doLoggingC = true;
      }

   function setLogFile($fileName) { $this->fileNameC = $fileName; }
   function setLogging($state) { $this->doLoggingC = $state; }

   function log($msg) {
      if ($this->doLoggingC == true) {
         file_put_contents($this->fileNameC, $msg, FILE_APPEND);
         }
      }

   function forceLog($msg) {
      file_put_contents($this->fileNameC, $msg, FILE_APPEND);
      }

   }
   ?>

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

Содержание db_config.php файла следующее. За исключением того, что здесь не представлены реальные пользователи, пароли и базы данных!

1
2
$db_con = mysql_connect("localhost", "theUser", "thePassword", true) or die(mysql_error());
$db_selected = mysql_select_db("theDatabase") or die(mysql_error());

И наконец, файл с паролем email («emailPassword.php» в представленном выше коде):

1
2
3
4
5
6
7
8
9
10
11
<?php

function hiddenEmailAccount() {
   return "someone@somewhere.com";
   }

function hiddenEmailPassword() {
   return "thePassword";
   }

?>

Надеюсь, представленная информация была полезной, если вы собираетесь заниматься интеграцией PayPal с помощью простого PHP класса. Спасибо за того, что прочитали мою статью и удачного вам программирования. Если вам удастся, что-то улучшить, буду рад увидеть улучшения в комментариях.

Чтобы не пропустить публикацию следующей статьи подписывайтесь на рассылку по E-mail или RSS ленту блога.

Нравится

Комментарии

  • /path_to/swift_required.php это что такое?

    • библиотека для работы с письмами swiftmailer.org

  • Oleksandr

    Добрый день!
    Хочу использовать Ваш класс у себя на сайте — как его правильно встроить в сайт — можно пример реальной странички для оплаты. Заранее благодарен.

  • Васян

    Мощно для 2013 года, эталонный говнокод. Ну хоть оформление норм.

    Данный класс не рекомендуется к использованию в продакшене, но для ознакомления сойдет.

    • Именно для ознакомления и предназначены все статьи и листинги данного ресурса.

    • Igor

      а что ты посоветуешь?

  • Оставить комментарий

    Подписаться на комментарии к этой статье по RSS

    Размещение статей и контекстных ссылок
    Яндекс.Метрика