Статьи

Как продавать цифровые товары с CodeIgniter

В сегодняшнем руководстве вы узнаете, как создать небольшое веб-приложение для безопасной продажи цифровых товаров (например, электронных книг) и приема платежей от PayPal.
Обратите внимание, что для этого руководства требуется PHP 5. Пожалуйста, обновите установку PHP, если вы все еще используете версию 4.


Загрузите последнюю версию CodeIgniter на свой сервер и настройте базу данных MySQL (я назвал ее digitalgoods ) следующими SQL-запросами:

01
02
03
04
05
06
07
08
09
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
CREATE TABLE `ci_sessions` (
  `session_id` varchar(40) NOT NULL DEFAULT ‘0’,
  `ip_address` varchar(16) NOT NULL DEFAULT ‘0’,
  `user_agent` varchar(50) NOT NULL,
  `last_activity` int(10) unsigned NOT NULL DEFAULT ‘0’,
  `user_data` text NOT NULL,
  PRIMARY KEY (`session_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
  
CREATE TABLE `downloads` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `item_id` int(11) DEFAULT NULL,
  `purchase_id` int(11) DEFAULT NULL,
  `download_at` int(11) DEFAULT NULL,
  `ip_address` varchar(15) DEFAULT NULL,
  `user_agent` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
  
CREATE TABLE `items` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `description` text,
  `price` decimal(10,2) DEFAULT NULL,
  `file_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
  
CREATE TABLE `purchases` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `item_id` int(11) DEFAULT NULL,
  `key` varchar(255) DEFAULT NULL,
  `email` varchar(127) DEFAULT NULL,
  `active` tinyint(1) DEFAULT NULL,
  `purchased_at` int(11) DEFAULT NULL,
  `paypal_email` varchar(127) DEFAULT NULL,
  `paypal_txn_id` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
  
INSERT INTO `items` (`id`,`name`,`description`,`price`,`file_name`)
VALUES
  (1, ‘Unix and CHMOD’, ‘Nulla fringilla, orci ac euismod semper, magna diam porttitor mauris, quis sollicitudin sapien justo in libero. Vestibulum mollis mauris enim. Morbi euismod magna ac lorem rutrum elementum.\n\nIn condimentum facilisis porta. Sed nec diam eu diam mattis viverra. Nulla fringilla, orci ac euismod semper, magna diam porttitor mauris, quis sollicitudin sapien justo in libero. Vestibulum mollis mauris enim. Morbi euismod magna ac lorem rutrum elementum. Donec viverra auctor lobortis. Pellentesque eu est a nulla placerat dignissim. Morbi a enim in magna semper bibendum. Etiam scelerisque, nunc ac egestas consequat, odio nibh euismod nulla, eget auctor orci nibh vel nisi.\n\nDonec viverra auctor lobortis. Pellentesque eu est a nulla placerat dignissim. Morbi a enim in magna semper bibendum. Etiam scelerisque, nunc. Morbi malesuada nulla nec purus convallis consequat. Vivamus id mollis quam. Morbi ac commodo nulla.’, 19.99, ‘UNIX and CHMOD.txt’),
  (2, ‘Intro to 8086 Programming’, ‘Nulla fringilla, orci ac euismod semper, magna diam porttitor mauris, quis sollicitudin sapien justo in libero. Vestibulum mollis mauris enim. Morbi euismod magna ac lorem rutrum elementum.\n\nMorbi malesuada nulla nec purus convallis consequat. Vivamus id mollis quam. Morbi ac commodo nulla.’, 4.95, ‘Intro to 8086 Programming.txt’);

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

Мы вставили два элемента в базу данных, поэтому нам нужно создать эти файлы на сервере. В корневом каталоге вашего приложения (в той же папке, что и system каталог CodeIgniter), создайте новый каталог с именем files :


В этом каталоге создайте два текстовых файла с именами UNIX and CHMOD.txt и Intro to 8086 Programming.txt . Заглавные буквы важны на большинстве веб-серверов. Это имена файлов, которые мы устанавливаем в базе данных для наших двух элементов. Введите некоторый контент в файлы, чтобы мы могли быть уверены, что файлы загружаются правильно.

В файле system/application/config/database.php настройте параметры базы данных в следующих полях:

1
2
3
4
$db[‘default’][‘hostname’] = «localhost»;
$db[‘default’][‘username’] = «root»;
$db[‘default’][‘password’] = «»;
$db[‘default’][‘database’] = «digitalgoods»;

В config/autoload.php установите следующие библиотеки и помощники:

1
2
3
$autoload[‘libraries’] = array( ‘database’, ‘session’, ‘form_validation’, ’email’ );
$autoload[‘helper’] = array( ‘url’, ‘form’, ‘download’, ‘file’ );

В config/config.php установите base_url:

1
$config[‘base_url’] = «http://localhost/digitalgoods/»;

В том же файле вставьте следующее, чтобы создать наши собственные параметры конфигурации:

01
02
03
04
05
06
07
08
09
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
/*
|—————————————————————————
|
|—————————————————————————
|
|
|
*/
$config[‘site_name’] = «Digital Goods»;
  
/*
|—————————————————————————
|
|—————————————————————————
|
|
|
*/
$config[‘admin_email’] = «[email protected]»;
  
/*
|—————————————————————————
|
|—————————————————————————
|
|
|
*/
$config[‘no_reply_email’] = «[email protected]»;
  
/*
|—————————————————————————
|
|—————————————————————————
|
|
|
*/
$config[‘paypal_email’] = «[email protected]»;
  
/*
|—————————————————————————
|
|—————————————————————————
|
|
|
|
*/
$config[‘download_limit’] = array(
    ‘enable’ => false,
    ‘downloads’ => ‘4’,
    ‘days’ => ‘7’
);

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

Наконец, в config/routes.php установите контроллер по умолчанию:

1
$route[‘default_controller’] = «items»;

Теперь, чтобы создать основной контроллер для сайта, создайте файл в каталоге controllers именем items.php и внутри введите:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<?php if (!defined(‘BASEPATH’)) exit(‘No direct script access allowed’);
  
class Items extends Controller {
  
    function Items() {
        parent::Controller();
        $this->load->model( ‘items_model’, ‘Item’ );
        $data[‘site_name’] = $this->config->item( ‘site_name’ );
        $this->load->vars( $data );
    }
  
    function index() {
        echo ‘Hello, World!’;
    }
  
}

В конструкторе мы загрузили модель ‘items_model’ (которую мы назвали ‘Item’), которую мы создадим далее, и поместили настройку конфигурации ‘site_name’ в переменную, к которой мы можем обращаться в представлениях. Для метода index мы просто установили простое «Hello, World!» сообщение на данный момент.

Кроме того, создайте новую модель, создав новый файл в каталоге models именем items_model.php и внутри введите:

1
2
3
4
5
6
7
8
9
<?php if (!defined(‘BASEPATH’)) exit(‘No direct script access allowed’);
  
class Items_model extends Model {
  
    function Items_model() {
        parent::Model();
    }
  
}

Если вы посмотрите на сайт в своем браузере сейчас, вы должны увидеть «Hello, World!» сообщение, которое подается от метода index() в контроллере.

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

В контроллере Items отредактируйте функцию index() чтобы использовать следующие строки:

1
2
3
4
5
$data[‘page_title’] = ‘All Items’;
$data[‘items’] = $this->Item->get_all();
$this->load->view( ‘header’, $data );
$this->load->view( ‘items/list’, $data );
$this->load->view( ‘footer’, $data );

В первой строке мы установили заголовок для страницы «Все элементы». Как обычно, все в массиве $data будет извлечено при его передаче в представление — поэтому к $data['page_title'] можно получить доступ просто как $page_title .

После этого мы вызываем метод get_all() из модели Item (которую мы еще не создали) и get_all() результат в переменную $items .

В трех следующих строках мы загружаем три файла вида.

Внутри каталога application/system/views/ создайте следующую структуру файлов / папок: ( header.php , footer.php и каталог items/ с index.php внутри).


header.php и footer.php являются универсальными в нашем приложении и будут использоваться на каждой странице, и для каждого контроллера мы создадим отдельную папку представлений ( items/ ), и у каждого метода будет файл представления, названный в его честь ( index.php ).

Внутри header.php введите следующий HTML5:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang=»en»>
<head>
  <meta charset=»UTF-8″ />
  <link rel=»stylesheet» href=»<?php echo base_url(); ?>css/style.css» type=»text/css» />
  <title><?php echo $page_title .
</head>
<body>
  
<div id=»wrap»>
  
  <header>
    <h1><?php echo anchor( », $site_name );
  </header>
  
  <section>

В строке 5 мы включаем файл CSS, который создадим позже, и который будет расположен в корне файловой структуры нашего сайта.

В строке 6 мы используем переменные $page_title и $site_title чтобы создать соответствующий заголовок для страницы.

Наконец, в строке 13 мы используем метод CodeIgniter anchor() (из URL Helper ) для создания ссылки на домашнюю страницу сайта.

Внутри footer.php введите:

01
02
03
04
05
06
07
08
09
10
11
12
  </section>
    
  <footer>
    <?php $copyright = ( date( ‘Y’ > 2010 ) ) ?
    <p><small>
      Copyright &copy;
    </small></p>
  </footer>
  
</div><!— /wrap —>
</body>
</html>

В строке 4 указываются даты уведомления об авторских правах. Если текущий год — 2010, то в уведомлении будет отображаться только 2010 год. Если текущий год более поздний, чем 2010, например, 2013, дата в уведомлении об авторском праве будет отображаться как «2010–2013».

Как вы помните, в контроллере мы get_all() метод get_all() из модели Items. Итак, внутри models/items_model.php введите:

1
2
3
function get_all() {
  return $this->db->get( ‘items’ )->result();
}

Эта строка кода извлекает все записи из таблицы «items» в базе данных как объекты PHP и возвращает их в массив.

Теперь, чтобы отобразить элементы, введите следующее в views/items/index.php :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<?php
if ( ! $items ) :
  echo ‘<p>No items found.</p>’;
  
else :
  echo ‘<h2>Items</h2>’;
  echo ‘<ul>’;
  
  foreach ( $items as $item ) {
    $segments = array( ‘item’, url_title( $item->name, ‘dash’, true ), $item->id );
    echo ‘<li>’ .
  }
  
  echo ‘</ul>’;
  
endif;

Сначала мы проверяем, извлекли ли мы какие-либо элементы из базы данных, и отображаем сообщение, в котором говорится, что элементов не найдено.

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

CodeIgniter возвращает каждую запись из базы данных как объект, поэтому вы можете получить доступ к деталям элемента с помощью $item->name , $item->id , $item->price и т. Д. Для каждого поля в базе данных.

Мы дадим каждому элементу SEO-дружественный URL с названием элемента. Например: http://example.com/item/unix-and-chmod/1/ (имя находится во втором сегменте и идентификатор в третьем).

В строке 10 мы устанавливаем переменную $segments для массива, содержащего данные, которые будут преобразованы в URL (каждый элемент в массиве будет сегментом URL). Первый сегмент — просто ‘item’, затем мы используем функцию url_title() CodeIgniter, чтобы сделать имя элемента url_title() для URL (удаляя заглавные буквы и заменяя пробелы черточками). И третий сегмент — это идентификатор товара.

На следующей строке мы создаем новый элемент списка и передаем $segments в anchor() для создания URL. Мы также отображаем цену товара. Затем цикл закрывается, список закрывается, а оператор if завершается.

Обновите страницу в вашем браузере, и вы увидите два элемента из базы данных:



Вы можете задаться вопросом, почему сегменты URL для одного элемента являются item/unix-and-chmod/1 как в CodeIgniter, что означает, что мы указываем на контроллер с именем ‘item’ с помощью метода ‘unix-and-chmod’, который не совсем имеет смысла, так как наш контроллер называется «items», и иметь отдельный метод для каждого элемента — это безумие. Ну, ты прав. Мы будем использовать «Маршруты» CodeIgniter для пересылки всех запросов «item» в метод «details» в нашем контроллере «items», чтобы помочь сократить наши URL.

Внутри system/application/config/routes.php после параметров ‘default_controller’ и ‘scaffolding_trigger’ добавьте:

1
$route[‘item/:any’] = ‘items/details’;

Этот фрагмент кода выполняет внутреннюю пересылку всех запросов на item/ (контроллер ‘item’) на items/details (контроллер ‘items’ — метод ‘details’).

Поэтому внутри controllers/items.php добавьте следующий метод:

1
2
3
function details() { // ROUTE: item/{name}/{id}
  echo ‘Hello, World!’;
}

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

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

Теперь мы уверены, что маршруты работают нормально, замените «Hello, World!» эхо заявление со следующим:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
$id = $this->uri->segment( 3 );
$item = $this->Item->get( $id );
  
if ( ! $item ) {
  $this->session->set_flashdata( ‘error’, ‘Item not found.’ );
  redirect( ‘items’ );
}
  
$data[‘page_title’] = $item->name;
$data[‘item’] = $item;
  
$this->load->view( ‘header’, $data );
$this->load->view( ‘items/details’, $data );
$this->load->view( ‘footer’, $data );

В первой строке мы берем идентификатор из третьего сегмента URL, а затем используем его для получения элемента с этим идентификатором (мы создадим метод get() в модели после).

Если элемент не может быть найден в базе данных, мы устанавливаем сообщение об ошибке «Элемент не найден» в сеансе пользователя и перенаправляем его обратно на главную страницу.

Мы устанавливаем заголовок страницы для имени элемента, делаем элемент доступным в представлении и загружаем соответствующие файлы представления.

Внутри models/items_model.php добавьте следующий метод:

1
2
3
4
5
function get( $id ) {
  $r = $this->db->where( ‘id’, $id )->get( ‘items’ )->result();
  if ( $r ) return $r[0];
  return false;
}

В первой строке мы запрашиваем у базы данных запись с указанным идентификатором в таблице «items». Мы получаем результат и сохраняем его в переменной $r .

Если запись была найдена, мы возвращаем первый элемент массива. В противном случае мы возвращаем false.

В нашем контроллере мы устанавливаем сообщение об ошибке в сеансе пользователя, используя set_flashdata() если элемент не найден. Чтобы отобразить эту ошибку в браузере, добавьте следующее в views/header.php :

1
2
3
4
5
6
7
<?php
if ( $this->session->flashdata( ‘success’ ) )
  echo ‘<p class=»success»>’ .
  
if ( $this->session->flashdata( ‘error’ ) )
  echo ‘<p class=»error»>’ .
?>

Это просто отображает сообщение об успехе или ошибке, если они существуют в сеансе пользователя.

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

Создайте файл с именем views/items/details.php и введите следующее:

1
2
3
4
5
6
<h2><?php echo $item->name .
  
<p><?php echo nl2br( $item->description );
  
<?php $segments = array( ‘purchase’, url_title( $item->name, ‘dash’, true ), $item->id );
<p class=»purchase»><?php echo anchor( $segments, ‘Purchase’ );

Здесь мы просто отображаем имя и цену элемента в заголовке, мы используем nl2br() для преобразования nl2br() в стиле SQL в описании элемента в теги <br /> стиле HTML. Затем мы создаем SEO-дружественную ссылку на страницу покупки (например, http://example.com/purchase/unix-and-chmod/1 ).



Сначала нам нужно добавить новый маршрут для прямых запросов на ссылку «покупка» на «товары / покупки» — так же, как мы делали со страницами с одним товаром. Добавьте следующее в config/routes.php :

1
$route[‘purchase/:any’] = ‘items/purchase’;

Теперь внутри controllers/items.php добавьте следующий метод «покупки»:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
function purchase() { // ROUTE: purchase/{name}/{id}
  $item_id = $this->uri->segment( 3 );
  $item = $this->Item->get( $item_id );
    
  if ( ! $item ) {
    $this->session->set_flashdata( ‘error’, ‘Item not found.’ );
    redirect( ‘items’ );
  }
    
  $data[‘page_title’] = ‘Purchase &ldquo;’
  $data[‘item’] = $item;
    
  $this->load->view( ‘header’, $data );
  $this->load->view( ‘items/purchase’, $data );
  $this->load->view( ‘footer’, $data );
}

По сути, это то же самое, что и вид отдельного товара, только с «Покупкой« … »в заголовке страницы, и вместо этого мы загружаем вид items/purchase .

Создайте файл в views/items/purchase.php со следующим внутри:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<h2>Purchase</h2>
  
<?php $segments = array( ‘item’, url_title( $item->name, ‘dash’, true ), $item->id );
<p>To purchase &ldquo;<?php echo anchor( $segments, $item->name );
address below and click through to pay with PayPal.
email you your download link to the address you enter below.</p>
  
<?php
$url_title = url_title( $item->name, ‘dash’, true );
echo form_open( ‘purchase/’ . $url_title . ‘/’ . $item->id );
echo validation_errors( ‘<p class=»error»>’, ‘</p>’ );
?>
  <p>
    <label for=»email»>Email:</label>
    <input type=»email» name=»email» id=»email» /> &nbsp;
    <input type=»submit» value=»Pay $<?php echo $item->price; ?> via PayPal» />
  </p>
</form>

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


Чтобы общаться с PayPal, мы собираемся использовать версию библиотеки PayPal Lib CodeIgniter от Ran Aroussi, которую я изменил, чтобы включить рекомендуемые изменения со страницы Wiki и добавить простую поддержку песочницы PayPal для целей разработки.

Создайте новый файл в папке application/config/ именем paypallib_config.php со следующим внутри:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<?php if (!defined(‘BASEPATH’)) exit(‘No direct script access allowed’);
  
// ————————————————————————
// Ppal (Paypal IPN Class)
// ————————————————————————
  
// If (and where) to log ipn to file
$config[‘paypal_lib_ipn_log_file’] = BASEPATH .
$config[‘paypal_lib_ipn_log’] = TRUE;
  
// Where are the buttons located at
$config[‘paypal_lib_button_path’] = ‘buttons’;
  
// What is the default currency?
$config[‘paypal_lib_currency_code’] = ‘USD’;
  
// Enable Sandbox mode?
$config[‘paypal_lib_sandbox_mode’] = TRUE;

А внутри application/libraries/ создайте файл с именем Paypal_Lib.php (заглавные буквы важны) со следующим внутри:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
<?php if (!defined(‘BASEPATH’)) exit(‘No direct script access allowed’);
/**
 * Code Igniter
 *
 * An open source application development framework for PHP 4.3.2 or newer
 *
 * @package CodeIgniter
 * @author Rick Ellis
 * @copyright Copyright (c) 2006, pMachine, Inc.
 * @license http://www.codeignitor.com/user_guide/license.html
 * @link http://www.codeigniter.com
 * @since Version 1.0
 * @filesource
 */
  
// ————————————————————————
  
/**
 * PayPal_Lib Controller Class (Paypal IPN Class)
 *
 * This CI library is based on the Paypal PHP class by Micah Carrick
 * See www.micahcarrick.com for the most recent version of this class
 * along with any applicable sample files and other documentaion.
 *
 * This file provides a neat and simple method to interface with paypal and
 * The paypal Instant Payment Notification (IPN) interface.
 * NOT intended to make the paypal integration «plug ‘n’ play».
 * requires the developer (that should be you) to understand the paypal
 * process and know the variables you want/need to pass to paypal to
 * achieve what you want.
 *
 * This class handles the submission of an order to paypal as well as the
 * processing an Instant Payment Notification.
 * This class enables you to mark points and calculate the time difference
 * between them.
 *
 * The class requires the use of the PayPal_Lib config file.
 *
 * @package CodeIgniter
 * @subpackage Libraries
 * @category Commerce
 * @author Ran Aroussi <[email protected]>
 * @copyright Copyright (c) 2006, http://aroussi.com/ci/
 *
 */
  
// ————————————————————————
  
class Paypal_Lib {
  
    var $last_error;
    var $ipn_log;
  
    var $ipn_log_file;
    var $ipn_response;
    var $ipn_data = array();
    var $fields = array();
  
    var $submit_btn = »;
    var $button_path = »;
  
    var $CI;
  
    function Paypal_Lib()
    {
        $this->CI =& get_instance();
        $this->CI->load->helper(‘url’);
        $this->CI->load->helper(‘form’);
        $this->CI->load->config(‘paypallib_config’);
  
        $this->paypal_url = ‘https://www.paypal.com/cgi-bin/webscr’;
        if ( $this->CI->config->item(‘paypal_lib_sandbox_mode’) )
            $this->paypal_url = ‘https://www.sandbox.paypal.com/cgi-bin/webscr’;
  
        $this->last_error = »;
        $this->ipn_response = »;
  
        $this->ipn_log_file = $this->CI->config->item(‘paypal_lib_ipn_log_file’);
        $this->ipn_log = $this->CI->config->item(‘paypal_lib_ipn_log’);
  
        $this->button_path = $this->CI->config->item(‘paypal_lib_button_path’);
  
        // populate $fields array with a few default values.
        // documentation for a list of fields and their data types.
        // values can be overwritten by the calling script.
        $this->add_field(‘rm’,’2′);
        $this->add_field(‘cmd’,’_xclick’);
  
        $this->add_field(‘currency_code’, $this->CI->config->item(‘paypal_lib_currency_code’));
        $this->add_field(‘quantity’, ‘1’);
        $this->button(‘Pay Now!’);
    }
  
    function button($value)
    {
        // changes the default caption of the submit button
        $this->submit_btn = form_submit(‘pp_submit’, $value);
    }
  
    function image($file)
    {
        $this->submit_btn = ‘<input type=»image» name=»add» src=»‘ . base_url() . $this->button_path .’/’. $file . ‘» border=»0″ />’;
    }
  
  
    function add_field($field, $value)
    {
        // adds a key=>value pair to the fields array, which is what will be
        // sent to paypal as POST variables.
        // array, it will be overwritten.
        $this->fields[$field] = $value;
    }
  
    function paypal_get_request_link() {
        $url = $this->paypal_url .
  
        foreach ( $this->fields as $name => $value )
            $url .= $name .
  
        return $url;
    }
  
    function paypal_auto_form()
    {
        // this function actually generates an entire HTML page consisting of
        // a form with hidden elements which is submitted to paypal via the
        // BODY element’s onLoad attribute.
        // any POST vars from you custom form before submitting to paypal.
        // basically, you’ll have your own form which is submitted to your script
        // to validate the data, which in turn calls this function to create
        // another hidden form and submit to paypal.
  
        $this->button(‘Click here if you\’re not automatically redirected…’);
  
        echo ‘<html>’ .
        echo ‘<head><title>Processing Payment…</title></head>’ .
        echo ‘<body onLoad=»document.forms[\’paypal_auto_form\’].submit();»>’ .
        echo ‘<p>Please wait, your order is being processed and you will be redirected to the paypal website.</p>’ .
        echo $this->paypal_form(‘paypal_auto_form’);
        echo ‘</body></html>’;
    }
  
    function paypal_form($form_name=’paypal_form’)
    {
        $str = »;
        $str .= ‘<form method=»post» action=»‘.$this->paypal_url.'» name=»‘.$form_name.'»/>’ .
        foreach ($this->fields as $name => $value)
            $str .= form_hidden($name, $value) .
        $str .= ‘<p>’.
        $str .= form_close() .
  
        return $str;
    }
  
    function validate_ipn()
    {
        // parse the paypal URL
        $url_parsed = parse_url($this->paypal_url);
  
        // generate the post string from the _POST vars aswell as load the
        // _POST vars into an arry so we can play with them from the calling
        // script.
        $post_string = »;
        if (isset($_POST))
        {
            foreach ($_POST as $field=>$value)
            { // str_replace(«\n», «\r\n», $value)
                    // put line feeds back to CR+LF as that’s how PayPal sends them out
                    // otherwise multi-line data will be rejected as INVALID
  
                $value = str_replace(«\n», «\r\n», $value);
                $this->ipn_data[$field] = $value;
                $post_string .= $field.’=’.urlencode(stripslashes($value)).’&’;
  
            }
        }
  
        $post_string.=»cmd=_notify-validate»;
  
        // open the connection to paypal
        $fp = fsockopen($url_parsed[‘host’],»80″,$err_num,$err_str,30);
        if(!$fp)
        {
            // could not open the connection.
            // will be in the log.
            $this->last_error = «fsockopen error no. $errnum: $errstr»;
            $this->log_ipn_results(false);
            return false;
        }
        else
        {
            // Post the data back to paypal
            fputs($fp, «POST $url_parsed[path] HTTP/1.1\r\n»);
            fputs($fp, «Host: $url_parsed[host]\r\n»);
            fputs($fp, «Content-type: application/x-www-form-urlencoded\r\n»);
            fputs($fp, «Content-length: «.strlen($post_string).»\r\n»);
            fputs($fp, «Connection: close\r\n\r\n»);
            fputs($fp, $post_string . «\r\n\r\n»);
  
            // loop through the response from the server and append to variable
            while(!feof($fp))
                $this->ipn_response .= fgets($fp, 1024);
  
            fclose($fp);
        }
  
        if (eregi(«VERIFIED»,$this->ipn_response))
        {
            // Valid IPN transaction.
            $this->log_ipn_results(true);
            return true;
        }
        else
        {
            // Invalid IPN transaction.
            $this->last_error = ‘IPN Validation Failed.’;
            $this->log_ipn_results(false);
            return false;
        }
    }
  
    function log_ipn_results($success)
    {
        if (!$this->ipn_log) return;
  
        // Timestamp
        $text = ‘[‘.date(‘m/d/Y g:i A’).’] — ‘;
  
        // Success or failure being logged?
        if ($success) $text .= «SUCCESS!\n»;
        else $text .= ‘FAIL: ‘.$this->last_error.»\n»;
  
        // Log the POST variables
        $text .= «IPN POST Vars from Paypal:\n»;
        foreach ($this->ipn_data as $key=>$value)
            $text .= «$key=$value, «;
  
        // Log the response from the paypal server
        $text .= «\nIPN Response from Paypal Server:\n «.$this->ipn_response;
  
        // Write to log
        $fp=fopen($this->ipn_log_file,’a’);
        fwrite($fp, $text . «\n\n»);
  
        fclose($fp);
    }
  
  
    function dump()
    {
        // Used for debugging, this function will output all the field/value pairs
        // that are currently defined in the instance of the class using the
        // add_field() function.
  
        ksort($this->fields);
        echo ‘<h2>ppal->dump() Output:</h2>’ .
        echo ‘<code style=»font: 12px Monaco, \’Courier New\’, Verdana, Sans-serif; background: #f9f9f9; border: 1px solid #D0D0D0; color: #002166; display: block; margin: 14px 0; padding: 12px 10px;»>’ .
        foreach ($this->fields as $key => $value) echo ‘<strong>’.
        echo «</code>\n»;
    }
  
}

Когда пользователь вводит свой адрес электронной почты в форму на странице «Покупка», мы хотим:

  1. Убедитесь, что пользователь ввел адрес электронной почты;
  2. Добавьте их адрес электронной почты в нашу базу данных вместе со случайным ключом, который мы будем использовать для ссылки на их покупку;
  3. Отправьте информацию об элементе вместе со случайным ключом пользователя в PayPal для обработки платежа.

Внутри controllers/items.php добавьте следующее перед $data['page_title'] = ... внутри метода purchase() :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
$this->form_validation->set_rules( ’email’, ‘Email’, ‘required|valid_email|max_length[127]’ );
  
if ( $this->form_validation->run() ) {
  $email = $this->input->post( ’email’ );
    
  $key = md5( $item_id . time() . $email . rand() );
  $this->Item->setup_payment( $item->id, $email, $key );
    
  $this->load->library( ‘Paypal_Lib’ );
  $this->paypal_lib->add_field( ‘business’, $this->config->item( ‘paypal_email’ ));
  $this->paypal_lib->add_field( ‘return’, site_url( ‘paypal/success’ ) );
  $this->paypal_lib->add_field( ‘cancel_return’, site_url( ‘paypal/cancel’ ) );
  $this->paypal_lib->add_field( ‘notify_url’, site_url( ‘paypal/ipn’ ) );
    
  $this->paypal_lib->add_field( ‘item_name’, $item->name );
  $this->paypal_lib->add_field( ‘item_number’, ‘1’ );
  $this->paypal_lib->add_field( ‘amount’, $item->price );
    
  $this->paypal_lib->add_field( ‘custom’, $key );
    
  redirect( $this->paypal_lib->paypal_get_request_link() );
}

В первой строке мы проверяем, что отправленное поле формы «email» является действительным адресом электронной почты. Все внутри цикла if в строке 3 будет работать, если поле ’email’ проверено.

В строке 6 мы генерируем уникальный ключ для текущей покупки, хешируя идентификатор товара, текущее время, адрес электронной почты пользователя и случайное число, используя хэш MD5.

Строка 7 добавляет идентификатор элемента, электронную почту пользователя и случайный ключ в базу данных с помощью setup_payment() который мы еще не создали.

Оставшаяся часть кода загружает библиотеку «Paypal_Lib» и добавляет сведения об элементе и сайте с использованием функций библиотеки PayPal. Мы также передаем случайный ключ, сгенерированный нами, в PayPal, используя поле «custom». PayPal отправит нам этот ключ после подтверждения оплаты, чтобы мы могли активировать загрузку пользователя.

Наконец, мы перенаправляем пользователя на страницу оплаты PayPal.

Теперь нам нужно создать setup_payment() в модели. Поэтому внутри models/items_model.php добавьте следующее:

1
2
3
4
5
6
7
8
9
function setup_payment( $item_id, $email, $key ) {
  $data = array(
    ‘item_id’ => $item_id,
    ‘key’ => $key,
    ’email’ => $email,
    ‘active’ => 0 // hasn’t been purchased yet
  );
  $this->db->insert( ‘purchases’, $data );
}

Это довольно просто: мы создаем массив, содержащий предоставленный идентификатор элемента, адрес электронной почты пользователя и случайный ключ. Мы также устанавливаем ‘active’ в 0 (это устанавливается в ‘1’ после подтверждения оплаты). Затем массив вставляется в таблицу «покупок».

С помощью системы, которую мы используем для взаимодействия с PayPal, после того, как пользователь завершил платеж, PayPal отправит пользователю URL-адрес «успеха», который мы предоставляем. На этой странице будет просто сказано: «Ваш платеж получен. В настоящее время мы обрабатываем его».

За кулисами PayPal отправит нашему «слушателю IPN» подтверждение оплаты и некоторые подробности о нем, а слушатель отправит эти данные обратно в PayPal, чтобы подтвердить подлинность сообщения. После того, как наш слушатель IPN получит второе подтверждение, мы обрабатываем покупку и активируем загрузку пользователя.

Мы собираемся создать новый контроллер для обработки этих запросов от PayPal. Итак, внутри controllers/ создайте файл с именем paypal.php и введите следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php if (!defined(‘BASEPATH’)) exit(‘No direct script access allowed’);
  
class Paypal extends Controller {
  
  function Paypal() {
    parent::Controller();
    $this->load->model( ‘items_model’, ‘Item’ );
    $this->load->library( ‘Paypal_Lib’ );
    $data[‘site_name’] = $this->config->item( ‘site_name’ );
    $this->load->vars( $data );
  }
  
  function index() {
    redirect( ‘items’ );
  }
  
  function success() {
    $this->session->set_flashdata( ‘success’, ‘Your payment is being processed now. Your download link will be emailed to your shortly.’ );
    redirect( ‘items’ );
  }
  
  function cancel() {
    $this->session->set_flashdata( ‘success’, ‘Payment cancelled.’ );
    redirect( ‘items’ );
    }
  
}

Это страницы «Успех» и «Отмена» («Отмена» используется, если пользователь не продолжает оплату и вместо этого нажимает кнопку «Отмена» в PayPal).

PayPal предоставляет разработчикам «песочницу» для тестирования своего кода. Вы можете создать свои собственные адреса PayPal для песочницы, чтобы отправлять платежи. Зарегистрируйте бесплатную учетную запись разработчика на https://developer.paypal.com/, затем перейдите к разделу «Создание предварительно настроенной учетной записи покупателя или продавца»:


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


Теперь вернитесь на сайт, который мы создаем, и кликните, чтобы купить предмет. Обратите внимание, что когда вы попадаете в PayPal, адресом является https://www. sandbox. paypal.com/.... https://www. sandbox. paypal.com/.... https://www. sandbox. paypal.com/.... Войдите с учетной записью покупателя, которую вы создали справа:


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


Основной интерфейс сайта теперь готов. Нам просто нужно добавить в наш слушатель IPN и отправить товар покупателю по электронной почте.


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

Функция IPN немного велика, поэтому я ее разобью. Внутри контроллера PayPal добавьте следующую функцию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function ipn() {
  if ( $this->paypal_lib->validate_ipn() ) {
    $item_name = $this->paypal_lib->ipn_data[‘item_name’];
    $price = $this->paypal_lib->ipn_data[‘mc_gross’];
    $currency = $this->paypal_lib->ipn_data[‘mc_currency’];
    $payer_email = $this->paypal_lib->ipn_data[‘payer_email’];
    $txn_id = $this->paypal_lib->ipn_data[‘txn_id’];
    $key = $this->paypal_lib->ipn_data[‘transaction_subject’];
      
    $this->Item->confirm_payment( $key, $payer_email, $txn_id );
    $purchase = $this->Item->get_purchase_by_key( $key );
    $item = $this->Item->get( $purchase->item_id );
  }
}

В самом начале мы проверяем данные, отправленные слушателю с помощью PayPal — обо всем этом заботится библиотека. Если данные действительны, мы получаем некоторые данные (название товара, цену, валюту, адрес электронной почты PayPal плательщика, идентификатор транзакции и уникальный ключ, который мы отправили в PayPal, когда начался процесс оплаты).

Затем мы можем использовать ключ для подтверждения платежа (установив для поля «active» значение «1») и добавить адрес электронной почты и идентификатор транзакции PayPal плательщика в базу данных для дальнейшего использования.

Используя ключ, мы можем получить информацию о покупке из базы данных, а также купленный предмет. Before continuing with the IPN function, let’s create the confirm_payment() and get_purchase_by_key() model methods.

So inside the model, add the following:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
function confirm_payment( $key, $paypal_email, $payment_txn_id ) {
  $data = array(
    'purchased_at' => time(),
    'active' => 1,
    'paypal_email' => $paypal_email,
    'paypal_txn_id' => $paypal_txn_id
  );
  $this->db->where( 'key', $key );
  $this->db->update( 'purchases', $data );
}
  
function get_purchase_by_key( $key ) {
  $r = $this->db->where( 'key', $key )->get( 'purchases' )->result();
  if ( $r ) return $r[0];
  return false;
}

The functions should be pretty self explanatory by now, so now we need to email the customer their download link. This is handled in the IPN listender, so back in the controller add the following to the end of the ipn() function:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// Send download link to customer
$to = $purchase->email;
$from = $this->config->item( 'no_reply_email' );
$name = $this->config->item( 'site_name' );
$subject = $item->name . ' Download';
  
$segments = array( 'item', url_title( $item->name, 'dash', true ), $item->id );
‘.
‘</p>’;
  
$this->email->from( $from, $name );
$this->email->to( $to );
$this->email->subject( $subject );
$this->email->message( $message );
$this->email->send();
$this->email->clear();

Here we’re using CodeIgniter’s Email class to send the email. We start by setting up ‘To’, ‘From’, ‘Name’ and ‘Subject’ variables with the relevant data.

We then write a short message for the body with a link to the file they purchased, followed by their download link (which will be in the format of: http://example.com/download/{key} ). Finally we add the variables into the Email class methods and send it.

The final thing we need in the IPN listener is to send the site’s admin an email with the transaction details. Add the following to the end of the ipn() function:

01
02
03
04
05
06
07
08
09
10
11
12
13
// Send confirmation of purchase to admin
$message = '<p><strong>New Purchase:</strong></p><ul>';
‘</li>’;
‘</li>’;
$message .= '<li><strong>Email:</strong> ' . $purchase->email . '</li><li></li>';
‘</li>’;
‘</li></ul>’;
$this->email->from( $from, $name );
$this->email->to( $this->config->item( 'admin_email' ) );
$this->email->subject( 'A purchase has been made' );
$this->email->message( $message );
$this->email->send();
$this->email->clear();

IMPORTANT! The IPN listener won’t work if you’re running on a local server (‘localhost’). Clearly if PayPal attempted to visit http://localhost/paypal/ipn/ they’re not going to arrive at your system. Upload your files to a remote server accessible by a domain name, or external IP address, for this to work.


The final step to getting the site fully working is to get the download links to work. When a customer goes to the download link we email them (eg. http://example.com/download/{key} ), we use their key to look up the download. If the purchase associated with the key is set to active (payment fulfilled) and the file exists on the server, the download will start.

First thing we need to do is add another route to set /download/ requests to go to items/download . Add the following to your config/routes.php file:

1
$route['download/:any'] = 'items/download';

Now, inside your items controller, add the following download() method:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function download() { // ROUTE: download/{purchase_key}
  $key = $this->uri->segment( 2 );
  $purchase = $this->Item->get_purchase_by_key( $key );
    
  // Check purchase was fulfilled
  if ( ! $purchase ) {
    $this->session->set_flashdata( 'error', 'Download key not valid.' );
    redirect( 'items' );
  }
  if ( $purchase->active == 0 ) {
    $this->session->set_flashdata( 'error', 'Download not active.' );
    redirect( 'items' );
  }
    
  // Get item and initiate download if exists
  $item = $this->Item->get( $purchase->item_id );
    
  $file_name = $item->file_name;
  $file_data = read_file( 'files/' . $file_name );
    
  if ( ! $file_data ) { // file not found on server
    $this->session->set_flashdata( 'error', 'The requested file was not found. Please contact us to resolve this.' );
    redirect( 'items' );
  }
      
  force_download( $file_name, $file_data );
}

In the first couple lines we lookup the purchase using the key in the URL. On line 6, if we can’t find a purchase record with that key, we set an error message that the key is invalid and redirect the user to the homepage. Similarly, on line 10, if the purchase was not fulfilled (active is ‘0’), we display an error.

We then retrieve the actual item from the database, and retrieve the file name. We use the read_file() method from CodeIgniter’s File helper to get the contents of the file. If the file can’t be found (or is empty), we display an error. Otherwise, we use the force_download() method to initiate a download.

We do have one problem—it’s possible to guess the name of a file and download it directly (ie. by visiting http://example.com/files/UNIX and CHMOD.txt ). To fix this, simply create a file named .htaccess inside the files/ directory with the following line inside:

1
deny from all

This tells the server to deny any requests for files in this directory – but the server itself can still access the files so buyers can still download using their own unique link.


At the root of your app (same location as the files and system directories), create a new directory named css , inside it create a file named style.css and add the following inside to spice things up a little:

01
02
03
04
05
06
07
08
09
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
body {
  background-color: #f9f9f9;
  color: #222;
  font-family: sans-serif;
}
  
#wrap {
  background-color: #fff;
  border: 1px solid #ddd;
  border-radius: 5px;
  -moz-border-radius: 5px;
  -webkit-border-radius: 5px;
  box-shadow: #e6e6e6 0 0 15px;
  -moz-box-shadow: #e6e6e6 0 0 15px;
  -webkit-box-shadow: #e6e6e6 0 0 15px;
  margin: 15px auto;
  padding: 15px;
  width: 760px;
}
  
a {
  color: #24badb;
  text-decoration: none;
}
  
header h1 {
  text-align: center;
}
  
header a {
  color: #222;
  padding: 7px 10px;
}
  
header a:hover {
  background-color: #222;
  color: #fff;
}
  
li {
  margin-bottom: 10px;
}
  
section {
  line-height: 1.5em;
}
  
section a {
  padding: 3px 4px;
}
  
section a:hover {
  background-color: #24badb;
  color: #fff;
}
  
footer {
  color: #bbb;
  text-align: right;
}
  
footer a {
  color: #bbb;
}
  
footer a:hover {
  color: #a0a0a0;
}


You may want to limit how often a user may download their purchase within a certain time period (perhaps to stop them sharing the download link around). Implementing this feature doesn’t take much work, so let’s add it in now.

We already set up a database table to log file downloads–’downloads’–which we haven’t used yet. We also have the ‘Download Limit’ setting in the config/config.php file, so go ahead and ‘enable’ it now:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
/*
|—————————————————————————
|
|—————————————————————————
|
|
|
|
*/
$config['download_limit'] = array(
    'enable' => true,
    'downloads' => '4',
    'days' => '7'
);

The default setting is to allow up to four file downloads in a seven day period. If, for example, the buyer tries to download five times in seven days, we’ll forward them back to the home page and display an error explaining why we can’t serve up their download right now.

The first thing we need to do is keep a log every time a download is initiated. To do this, add the following directly before the force_download(...) statement at the end of the download() function in the Items controller:

1
$this->Item->log_download( $item->id, $purchase->id, $this->input->ip_address(), $this->input->user_agent() );

Here we’re sending the item id, purchase id and the user’s IP address and user agent to a log_download() method in the Items model which we’ll create next.

Add the following method to your Items_model:

01
02
03
04
05
06
07
08
09
10
function log_download( $item_id, $purchase_id, $ip_address, $user_agent ) {
  $data = array(
    'item_id' => $item_id,
    'purchase_id' => $purchase_id,
    'download_at' => time(),
    'ip_address' => $ip_address,
    'user_agent' => $user_agent
  );
  $this->db->insert( 'downloads', $data );
}

This simply adds the data we provided, and the current time, to the ‘downloads’ table.

We’ll also need a method to get the downloads of a purchase, so add the following to the model:

1
2
3
function get_purchase_downloads( $purchase_id, $limit ) {
  return $this->db->where( 'purchase_id', $purchase_id )->limit( $limit )->order_by( 'id', 'desc' )->get( 'downloads' )->result();
}

Now, to actually add in the download limit, find the following piece of code in the download() method in the Items controller:

1
2
3
4
5
6
7
8
9
// Check purchase was fulfilled
if ( ! $purchase ) {
  $this->session->set_flashdata( 'error', 'Download key not valid.' );
  redirect( 'items' );
}
if ( $purchase->active == 0 ) {
  $this->session->set_flashdata( 'error', 'Download not active.' );
  redirect( 'items' );
}

Directly after this, add the following:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
// Check download limit
$download_limit = $this->config->item( 'download_limit' );
if ( $download_limit['enable'] ) {
  $downloads = $this->Item->get_purchase_downloads( $purchase->id, $download_limit['downloads'] );
  $count = 0;
  $time_limit = time() - (86400 * $download_limit['days']);
  foreach ( $downloads as $download ) {
    if ( $download->download_at >= $time_limit )
      $count++; // download within past x days
    else
      break;
  }
  
  // If over download limit, error
  if ( $count >= $download_limit['downloads'] ) { // can only download x times within y days
    $this->session->set_flashdata( 'error', 'You can only download a file ' . $download_limit['downloads'] . ' times in a ' . $download_limit['days'] . ' day period. Please try again later.' );
    redirect( 'items' );
  }
}

On the third line we check whether the ‘Download Limit’ functionality is enabled in the config file. If it is, we retrieve the downloads of the current purchase (limited to how many file downloads is permitted).

At line 6, we calculate the furthest time away downloads are limited to (eg. if we have a limit of 4 downloads in 7 days, we find the time 7 days ago) by multiplying the number of days by 86400 (the number of seconds in a day) and subtracting it from the current time.

The loop starting at line 7 checks each download logged to see if it was downloaded within the time limit (eg. 7 days). If it is, we increase $count , otherwise, we break out of the loop as we know if this logged download is older than the limit, all subsequent logs will be to.

At line 15, if the $count is greater than the number of downloads allowed, we display an error message. Otherwise, the rest of the code will be executed and the download will be logged and initiated.


Были сделаны. Попробуйте!

Note: To disable the PayPal ‘sandbox’ mode so you can recieve real payments, change the $config['paypal_lib_sandbox_mode'] option in config/paypallib_config.php to false .