1
|
|
|
<?php |
|
|
|
|
2
|
|
|
// This file is part of Moodle - http://moodle.org/ |
3
|
|
|
// |
4
|
|
|
// Moodle is free software: you can redistribute it and/or modify |
5
|
|
|
// it under the terms of the GNU General Public License as published by |
6
|
|
|
// the Free Software Foundation, either version 3 of the License, or |
7
|
|
|
// (at your option) any later version. |
8
|
|
|
// |
9
|
|
|
// Moodle is distributed in the hope that it will be useful, |
10
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of |
11
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12
|
|
|
// GNU General Public License for more details. |
13
|
|
|
// |
14
|
|
|
// You should have received a copy of the GNU General Public License |
15
|
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>. |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Listens for Instant Payment Notification from PayPal |
19
|
|
|
* |
20
|
|
|
* This script waits for Payment notification from PayPal, |
21
|
|
|
* then double checks that data by sending it back to PayPal. |
22
|
|
|
* If PayPal verifies this then sets the activity as completed. |
23
|
|
|
* |
24
|
|
|
* @package availability_paypal |
25
|
|
|
* @copyright 2010 Eugene Venter |
26
|
|
|
* @copyright 2015 Daniel Neis |
27
|
|
|
* @author Eugene Venter - based on code by others |
28
|
|
|
* @author Daniel Neis - based on code by others |
29
|
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later |
30
|
|
|
*/ |
31
|
|
|
|
32
|
|
|
require("../../../config.php"); |
33
|
|
|
require_once($CFG->libdir.'/eventslib.php'); |
34
|
|
|
require_once($CFG->libdir . '/filelib.php'); |
35
|
|
|
|
36
|
|
|
// PayPal does not like when we return error messages here, |
37
|
|
|
// the custom handler just logs exceptions and stops. |
38
|
|
|
set_exception_handler('availability_paypal_ipn_exception_handler'); |
39
|
|
|
|
40
|
|
|
// Keep out casual intruders. |
41
|
|
|
if (empty($_POST) or !empty($_GET)) { |
|
|
|
|
42
|
|
|
print_error("Sorry, you can not use the script that way."); |
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
|
46
|
|
|
// Read all the data from PayPal and get it ready for later; |
47
|
|
|
// we expect only valid UTF-8 encoding, it is the responsibility |
48
|
|
|
// of user to set it up properly in PayPal business account, |
49
|
|
|
// it is documented in docs wiki. |
50
|
|
|
$req = 'cmd=_notify-validate'; |
51
|
|
|
|
52
|
|
|
foreach ($_POST as $key => $value) { |
53
|
|
|
$req .= "&$key=".urlencode($value); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
$data = new stdclass(); |
57
|
|
|
$data->business = optional_param('business', '', PARAM_TEXT); |
58
|
|
|
$data->receiver_email = optional_param('receiver_email', '', PARAM_TEXT); |
59
|
|
|
$data->receiver_id = optional_param('receiver_id', '', PARAM_TEXT); |
60
|
|
|
$data->item_name = optional_param('item_name', '', PARAM_TEXT); |
61
|
|
|
$data->memo = optional_param('memo', '', PARAM_TEXT); |
62
|
|
|
$data->tax = optional_param('tax', '', PARAM_TEXT); |
63
|
|
|
$data->option_name1 = optional_param('option_name1', '', PARAM_TEXT); |
64
|
|
|
$data->option_selection1_x = optional_param('option_selection1_x', '', PARAM_TEXT); |
65
|
|
|
$data->option_name2 = optional_param('option_name2', '', PARAM_TEXT); |
66
|
|
|
$data->option_selection2_x = optional_param('option_selection2_x', '', PARAM_TEXT); |
67
|
|
|
$data->payment_status = optional_param('payment_status', '', PARAM_TEXT); |
68
|
|
|
$data->pending_reason = optional_param('pending_reason', '', PARAM_TEXT); |
69
|
|
|
$data->reason_code = optional_param('reason_code', '', PARAM_TEXT); |
70
|
|
|
$data->txn_id = optional_param('txn_id', '', PARAM_TEXT); |
71
|
|
|
$data->parent_txn_id = optional_param('parent_txn_id', '', PARAM_TEXT); |
72
|
|
|
$data->payment_type = optional_param('payment_type', '', PARAM_TEXT); |
73
|
|
|
$data->payment_gross = optional_param('mc_gross', '', PARAM_TEXT); |
74
|
|
|
$data->payment_currency = optional_param('mc_currency', '', PARAM_TEXT); |
75
|
|
|
$custom = optional_param('custom', '', PARAM_TEXT); |
76
|
|
|
$custom = explode('-', $custom); |
77
|
|
|
$data->userid = (int)$custom[0]; |
78
|
|
|
$data->contextid = (int)$custom[1]; |
79
|
|
|
$data->timeupdated = time(); |
80
|
|
|
|
81
|
|
|
if (! $user = $DB->get_record("user", array("id" => $data->userid))) { |
82
|
|
|
availability_paypal_message_error_to_admin("Not a valid user id", $data); |
83
|
|
|
die; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
if (! $context = context::instance_by_id($data->contextid, IGNORE_MISSING)) { |
87
|
|
|
availability_paypal_message_error_to_admin("Not a valid context id", $data); |
88
|
|
|
die; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
$instanceid = $context->instanceid; |
92
|
|
View Code Duplication |
if ($context instanceof context_module) { |
|
|
|
|
93
|
|
|
$availability = $DB->get_field('course_modules', 'availability', array('id' => $instanceid), MUST_EXIST); |
94
|
|
|
$availability = json_decode($availability); |
95
|
|
|
foreach ($availability->c as $condition) { |
96
|
|
|
if ($condition->type == 'paypal') { |
97
|
|
|
// TODO: handle more than one paypal for this context. |
98
|
|
|
$paypal = $condition; |
99
|
|
|
break; |
100
|
|
|
} else { |
101
|
|
|
availability_paypal_message_error_to_admin("Not a valid context id", $data); |
102
|
|
|
} |
103
|
|
|
} |
104
|
|
|
} else { |
105
|
|
|
// TODO: handle sections. |
106
|
|
|
print_error('support to sections not yet implemented.'); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
// Open a connection back to PayPal to validate the data. |
110
|
|
|
$paypaladdr = empty($CFG->usepaypalsandbox) ? 'www.paypal.com' : 'www.sandbox.paypal.com'; |
111
|
|
|
$c = new curl(); |
112
|
|
|
$options = array( |
113
|
|
|
'returntransfer' => true, |
114
|
|
|
'httpheader' => array('application/x-www-form-urlencoded', "Host: $paypaladdr"), |
115
|
|
|
'timeout' => 30, |
116
|
|
|
'CURLOPT_HTTP_VERSION' => CURL_HTTP_VERSION_1_1, |
117
|
|
|
); |
118
|
|
|
$location = "https://{$paypaladdr}/cgi-bin/webscr"; |
119
|
|
|
$result = $c->post($location, $req, $options); |
120
|
|
|
|
121
|
|
|
if (!$result) { // Could not connect to PayPal - FAIL. |
122
|
|
|
echo "<p>Error: could not access paypal.com</p>"; |
123
|
|
|
availability_paypal_message_error_to_admin("Could not access paypal.com to verify payment", $data); |
124
|
|
|
die; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
// Connection is OK, so now we post the data to validate it. |
128
|
|
|
|
129
|
|
|
// Now read the response and check if everything is OK. |
130
|
|
|
|
131
|
|
|
if (strlen($result) > 0) { |
132
|
|
|
if (strcmp($result, "VERIFIED") == 0) { // VALID PAYMENT! |
133
|
|
|
|
134
|
|
|
$DB->insert_record("availability_paypal_tnx", $data); |
135
|
|
|
|
136
|
|
|
// Check the payment_status and payment_reason. |
137
|
|
|
|
138
|
|
|
// If status is not completed, just tell admin, transaction will be saved later. |
139
|
|
|
if ($data->payment_status != "Completed" and $data->payment_status != "Pending") { |
|
|
|
|
140
|
|
|
availability_paypal_message_error_to_admin("Status not completed or pending. User payment status updated", $data); |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
// If currency is incorrectly set then someone maybe trying to cheat the system. |
144
|
|
|
if ($data->payment_currency != $paypal->currency) { |
145
|
|
|
availability_paypal_message_error_to_admin("Currency does not match course settings, received: ".$data->payment_currency, $data); |
146
|
|
|
die; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
// If status is pending and reason is other than echeck, |
150
|
|
|
// then we are on hold until further notice. |
151
|
|
|
// Email user to let them know. Email admin. |
152
|
|
|
if ($data->payment_status == "Pending" and $data->pending_reason != "echeck") { |
|
|
|
|
153
|
|
|
|
154
|
|
|
$eventdata = new \core\message\message(); |
155
|
|
|
$eventdata->component = 'availability_paypal'; |
156
|
|
|
$eventdata->name = 'payment_pending'; |
157
|
|
|
$eventdata->userfrom = get_admin(); |
158
|
|
|
$eventdata->userto = $user; |
159
|
|
|
$eventdata->subject = get_string("paypalpaymentpendingsubject", 'availability_paypal'); |
160
|
|
|
$eventdata->fullmessage = get_string('paypalpaymentpendingmessage', 'availability_paypal'); |
161
|
|
|
$eventdata->fullmessageformat = FORMAT_PLAIN; |
162
|
|
|
$eventdata->fullmessagehtml = ''; |
163
|
|
|
$eventdata->smallmessage = ''; |
164
|
|
|
message_send($eventdata); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
// If our status is not completed or not pending on an echeck clearance then ignore and die. |
168
|
|
|
// This check is redundant at present but may be useful if paypal extend the return codes in the future. |
169
|
|
|
if (! ( $data->payment_status == "Completed" or |
|
|
|
|
170
|
|
|
($data->payment_status == "Pending" and $data->pending_reason == "echeck") ) ) { |
|
|
|
|
171
|
|
|
die; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
// At this point we only proceed with a status of completed or pending with a reason of echeck. |
175
|
|
|
|
176
|
|
|
// Make sure this transaction doesn't exist already. |
177
|
|
|
if ($existing = $DB->get_record("availability_paypal_tnx", array("txn_id" => $data->txn_id))) { |
178
|
|
|
availability_paypal_message_error_to_admin("Transaction $data->txn_id is being repeated!", $data); |
179
|
|
|
die; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
// Check that the email is the one we want it to be. |
183
|
|
|
if (core_text::strtolower($data->business) !== core_text::strtolower($paypal->businessemail)) { |
184
|
|
|
availability_paypal_message_error_to_admin("Business email is {$data->business} (not ". |
185
|
|
|
$paypal->businessemail.")", $data); |
186
|
|
|
die; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
// Check that user exists. |
190
|
|
|
if (!$user = $DB->get_record('user', array('id' => $data->userid))) { |
191
|
|
|
availability_paypal_message_error_to_admin("User {$data->userid} doesn't exist", $data); |
192
|
|
|
die; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
// Check that course exists. |
196
|
|
|
if (!$course = $DB->get_record('course', array('id' => $data->courseid))) { |
197
|
|
|
availability_paypal_message_error_to_admin("Course {$data->courseid} doesn't exist", $data); |
198
|
|
|
die; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
$coursecontext = context_course::instance($course->id, IGNORE_MISSING); |
202
|
|
|
|
203
|
|
|
// Check that amount paid is the correct amount. |
204
|
|
|
if ( (float) $paypal->cost < 0 ) { |
205
|
|
|
$cost = (float) 0; |
206
|
|
|
} else { |
207
|
|
|
$cost = (float) $paypal->cost; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
// Use the same rounding of floats as on the plugin form. |
211
|
|
|
$cost = format_float($cost, 2, false); |
212
|
|
|
|
213
|
|
|
if ($data->payment_gross < $cost) { |
214
|
|
|
availability_paypal_message_error_to_admin("Amount paid is not enough ({$data->payment_gross} < {$cost}))", $data); |
215
|
|
|
die; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
// All clear! |
219
|
|
|
|
220
|
|
|
// Pass $view=true to filter hidden caps if the user cannot see them. |
221
|
|
|
if ($users = get_users_by_capability($context, 'moodle/course:update', 'u.*', 'u.id ASC', |
222
|
|
|
'', '', '', '', false, true)) { |
223
|
|
|
$users = sort_by_roleassignment_authority($users, $context); |
224
|
|
|
$teacher = array_shift($users); |
225
|
|
|
} else { |
226
|
|
|
$teacher = false; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/* |
|
|
|
|
230
|
|
|
$mailstudents = $paypal->mailstudents; |
231
|
|
|
$mailteachers = $paypal->mailteachers; |
232
|
|
|
$mailadmins = $paypal->mailadmins; |
233
|
|
|
$shortname = format_string($course->shortname, true, array('context' => $context)); |
234
|
|
|
|
235
|
|
|
if (!empty($mailstudents)) { |
236
|
|
|
$a = new stdClass(); |
237
|
|
|
$a->coursename = format_string($course->fullname, true, array('context' => $coursecontext)); |
238
|
|
|
$a->profileurl = "$CFG->wwwroot/user/view.php?id=$user->id"; |
239
|
|
|
|
240
|
|
|
$eventdata = new \core\message\message(); |
241
|
|
|
$eventdata->component = 'availability_paypal'; |
242
|
|
|
$eventdata->name = 'payment_completed'; |
243
|
|
|
$eventdata->userfrom = empty($teacher) ? core_user::get_support_user() : $teacher; |
244
|
|
|
$eventdata->userto = $user; |
245
|
|
|
$eventdata->subject = get_string("paypalpaymentcompletedsubject", 'paypal'); |
246
|
|
|
$eventdata->fullmessage = get_string('paypalpaymentcompletedmessage', 'paypal'); |
247
|
|
|
$eventdata->fullmessageformat = FORMAT_PLAIN; |
248
|
|
|
$eventdata->fullmessagehtml = ''; |
249
|
|
|
$eventdata->smallmessage = ''; |
250
|
|
|
message_send($eventdata); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
if (!empty($mailteachers) && !empty($teacher)) { |
254
|
|
|
$a->course = format_string($course->fullname, true, array('context' => $coursecontext)); |
255
|
|
|
$a->user = fullname($user); |
256
|
|
|
|
257
|
|
|
$eventdata = new \core\message\message(); |
258
|
|
|
$eventdata->component = 'availability_paypal'; |
259
|
|
|
$eventdata->name = 'payment_completed'; |
260
|
|
|
$eventdata->userfrom = $user; |
261
|
|
|
$eventdata->userto = $teacher; |
262
|
|
|
$eventdata->subject = get_string("paypalpaymentcompletedsubject", 'paypal'); |
263
|
|
|
$eventdata->fullmessage = get_string('paypalpaymentcompletedmessage', 'paypal'); |
264
|
|
|
$eventdata->fullmessageformat = FORMAT_PLAIN; |
265
|
|
|
$eventdata->fullmessagehtml = ''; |
266
|
|
|
$eventdata->smallmessage = ''; |
267
|
|
|
message_send($eventdata); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
if (!empty($mailadmins)) { |
271
|
|
|
$a->course = format_string($course->fullname, true, array('context' => $coursecontext)); |
272
|
|
|
$a->user = fullname($user); |
273
|
|
|
$admins = get_admins(); |
274
|
|
|
foreach ($admins as $admin) { |
275
|
|
|
$eventdata = new \core\message\message(); |
276
|
|
|
$eventdata->component = 'availability_paypal'; |
277
|
|
|
$eventdata->name = 'payment_completed'; |
278
|
|
|
$eventdata->userfrom = $user; |
279
|
|
|
$eventdata->userto = $admin; |
280
|
|
|
$eventdata->subject = get_string("paypalpaymentcompletedsubject", 'paypal'); |
281
|
|
|
$eventdata->fullmessage = get_string('paypalpaymentcompletedmessage', 'paypal'); |
282
|
|
|
$eventdata->fullmessageformat = FORMAT_PLAIN; |
283
|
|
|
$eventdata->fullmessagehtml = ''; |
284
|
|
|
$eventdata->smallmessage = ''; |
285
|
|
|
message_send($eventdata); |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
*/ |
289
|
|
|
|
290
|
|
|
} else if (strcmp ($result, "INVALID") == 0) { // ERROR. |
291
|
|
|
$DB->insert_record("availability_paypal_tnx", $data, false); |
292
|
|
|
availability_paypal_message_error_to_admin("Received an invalid payment notification!! (Fake payment?)", $data); |
293
|
|
|
} |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
function availability_paypal_message_error_to_admin($subject, $data) { |
297
|
|
|
$admin = get_admin(); |
298
|
|
|
$site = get_site(); |
299
|
|
|
|
300
|
|
|
$message = "$site->fullname: Transaction failed:{$subject}"; |
301
|
|
|
|
302
|
|
|
foreach ($data as $key => $value) { |
303
|
|
|
$message .= "{$key} => {$value};"; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
$eventdata = new stdClass(); |
307
|
|
|
$eventdata->component = 'availability_paypal'; |
308
|
|
|
$eventdata->name = 'payment_error'; |
309
|
|
|
$eventdata->userfrom = $admin; |
310
|
|
|
$eventdata->userto = $admin; |
311
|
|
|
$eventdata->subject = "PayPal ERROR: ".$subject; |
312
|
|
|
$eventdata->fullmessage = $message; |
313
|
|
|
$eventdata->fullmessageformat = FORMAT_PLAIN; |
314
|
|
|
$eventdata->fullmessagehtml = ''; |
315
|
|
|
$eventdata->smallmessage = ''; |
316
|
|
|
message_send($eventdata); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* Silent exception handler. |
321
|
|
|
* |
322
|
|
|
* @param Exception $ex |
323
|
|
|
* @return void - does not return. Terminates execution! |
324
|
|
|
*/ |
325
|
|
|
function availability_paypal_ipn_exception_handler($ex) { |
326
|
|
|
$info = get_exception_info($ex); |
327
|
|
|
|
328
|
|
|
$logerrmsg = "availability_paypal IPN exception handler: ".$info->message; |
329
|
|
|
if (debugging('', DEBUG_NORMAL)) { |
330
|
|
|
$logerrmsg .= ' Debug: '.$info->debuginfo."\n".format_backtrace($info->backtrace, true); |
331
|
|
|
} |
332
|
|
|
mtrace($logerrmsg); |
333
|
|
|
exit(0); |
|
|
|
|
334
|
|
|
} |
335
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.