From 48a9a16eeeade25191ea421d688cc81a6f4bc14c Mon Sep 17 00:00:00 2001 From: Sam White <webmaster@ycra.org.uk> Date: Fri, 10 Sep 2021 21:35:03 +0000 Subject: [PATCH] Allow purchasing of YCRA subscriptions. --- config.php.sample | 11 +- public_html/css/common.css | 4 + public_html/css/fields.css | 14 ++ public_html/includes/database.php | 58 ++++++++ public_html/includes/fields.php | 49 +++++++ public_html/includes/form-validation.php | 69 ++++++++++ public_html/includes/navbar.php | 6 +- public_html/includes/utils.php | 11 ++ public_html/join.php | 161 +++++++++++++++++++++++ 9 files changed, 379 insertions(+), 4 deletions(-) create mode 100644 public_html/css/fields.css create mode 100644 public_html/includes/database.php create mode 100644 public_html/includes/fields.php create mode 100644 public_html/includes/form-validation.php create mode 100644 public_html/join.php diff --git a/config.php.sample b/config.php.sample index 29381f2..e060c36 100644 --- a/config.php.sample +++ b/config.php.sample @@ -2,6 +2,15 @@ function get_config() { return [ 'site_root' => '/', - 'title_append' => 'DEV SITE', + 'title_append' => '', + ]; +} + +function get_db_details() { + return [ + 'host' => 'localhost', + 'database' => 'DB', + 'user' => 'USER', + 'password' => "PASSWORD", ]; } diff --git a/public_html/css/common.css b/public_html/css/common.css index 754bd9b..0aa5089 100644 --- a/public_html/css/common.css +++ b/public_html/css/common.css @@ -39,6 +39,10 @@ div.section { clear: both; } +.error { + color: red; +} + /* General styling for tables. */ table { /* Centre tables. */ diff --git a/public_html/css/fields.css b/public_html/css/fields.css new file mode 100644 index 0000000..66cb445 --- /dev/null +++ b/public_html/css/fields.css @@ -0,0 +1,14 @@ +div.field { + display: block; + margin: 1em 0; +} + +div.field label { + font-weight: bold; + display: block; +} + +div.field.required label::before { + content: '*'; + color: red; +} diff --git a/public_html/includes/database.php b/public_html/includes/database.php new file mode 100644 index 0000000..4aa870f --- /dev/null +++ b/public_html/includes/database.php @@ -0,0 +1,58 @@ +<?php +require_once('includes/config.php'); + +function db_connect() { + global $db_conn; + if (!$db_conn) { + $db = get_db_details(); + $db_conn = new mysqli($db['host'], $db['user'], $db['password'], $db['database']); + if ($db_conn->connect_error) + die('Database connection failed: ' . $db_conn->connect_error); + } +} + +function run_sql($sql) { + global $db_conn; + if (!$db_conn) db_connect(); + $result = $db_conn->query($sql); + if ($result === false) { + error_log('Error executing SQL query: ' . $db_conn->error + . ' while running the SQL: ' . $sql); + die('Database query failed! Error: ' . $db_conn->error); + } + return $result; +} + +function escape_mysql_string($string) { + global $db_conn; + if (!$db_conn) db_connect(); + return $db_conn->real_escape_string($string); +} + +function mysql_quote_value($value) { + if (is_null($value)) return 'NULL'; + return sprintf("'%s'", escape_mysql_string($value)); +} + +function simple_where($key, $value, $comp='=') { + return sprintf("%s $comp %s", $key, mysql_quote_value($value)); +} + +function record_exists($table, $where) { + $sql = "SELECT EXISTS(SELECT * FROM $table WHERE $where)"; + $result = run_sql($sql); + $val = mysqli_fetch_row($result)[0]; + if ($val) return true; + return false; +} + +function insert_array($table, $fields) { + $keys = $values = []; + foreach ($fields as $key=>$value) { + $keys[] = $key; + $values[] = mysql_quote_value($value); + } + $sql = "INSERT INTO $table (" . implode(',', $keys) . ') ' + . 'VALUES (' . implode(',', $values) . ')'; + return run_sql($sql); +} diff --git a/public_html/includes/fields.php b/public_html/includes/fields.php new file mode 100644 index 0000000..2419528 --- /dev/null +++ b/public_html/includes/fields.php @@ -0,0 +1,49 @@ +<?php +require_once('includes/utils.php'); + + +// TODO: Currently trivial, but we will want to handle more complicated cases +// later. +function get_sent_field_value($values, $name) { + if (!empty($values[$name])) return $values[$name]; + return ''; +} +function get_field_id($name) { + return $name; +} + +function general_bare_field($type, $values, $name, $attrs=[]) { + $value = get_sent_field_value($values, $name); + $id = get_field_id($name);?> + + <input type="<?php esc($type);?>" name="<?php esc($name);?>" + id="<?php esc($id);?>" value="<?php esc($value);?>"<?php + foreach($attrs as $name=>$value) esc("$name=\"$value\" ");?> + /><?php +} + +function field_label($name, $label) { + $id = get_field_id($name);?> + <label for="<?php esc($id);?>"><?php esc($label);?></label><?php +} + +function general_field($type, $values, $name, $label, $attrs=[]) {?> + <div class="field<?php esc(array_key_exists('required', $attrs) ? ' required' + : '');?>"><?php + field_label($name, $label); + general_bare_field($type, $values, $name, $attrs);?> + </div><?php +} + +function text_field($values, $name, $label, $attrs=[]) { + general_field('text', $values, $name, $label, $attrs); +} + +function email_field($values, $name, $label, $attrs=[]) { + general_field('email', $values, $name, $label, $attrs); +} + +function hidden_field($name, $value) { + general_bare_field('hidden', [$name=>$value], $name); +} +?> diff --git a/public_html/includes/form-validation.php b/public_html/includes/form-validation.php new file mode 100644 index 0000000..43ab85e --- /dev/null +++ b/public_html/includes/form-validation.php @@ -0,0 +1,69 @@ +<?php +require_once('includes/utils.php'); + +function require_field(&$errors, $data, $key, $name=null) { + if (is_null($name)) $name = key_to_human_text($key); + + if (empty($data[$key])) $errors[] = "The $name field must be filled in."; + return empty($errors); +} + +/** +* Validate an email address. +* Provide email address (raw input) +* Returns true if the email address has the email +* address format and the domain exists. +* +* Adapted from https://www.linuxjournal.com/article/9585 +*/ +function validate_email_address(&$errors, $email) { + $isValid = true; + $atIndex = strrpos($email, "@"); + if (is_bool($atIndex) && !$atIndex) { + $errors[] = "Missing '@' symbol in email address."; + return false; + } + $domain = substr($email, $atIndex+1); + $local = substr($email, 0, $atIndex); + + $localLen = strlen($local); + if ($localLen < 1 || $localLen > 64) { + $errors[] = 'Invalid length for local part of email address.'; + return false; + } + $domainLen = strlen($domain); + if ($domainLen < 1 || $domainLen > 255) { + $errors[] = 'Invalid length for domain part of email address.'; + return false; + } + + if ($local[0] == '.' || $local[$localLen-1] == '.' + || preg_match('/\\.\\./', $local)) { + $errors[] = 'Invalid format for local part of email address.'; + return false; + } + + if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) { + $errors[] = 'Invalid character in domain part of email address.'; + return false; + } + + if (preg_match('/\\.\\./', $domain)) { + $errors[] = 'Invalid format for domain part of email address.'; + return false; + } + + if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', + str_replace("\\\\","",$local)) + && !preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\","",$local))) { + $errors[] = 'Invalid character(s) in local part of email address - please ' + . 'enclose in quotation marks.'; + return false; + } + + if (!(checkdnsrr($domain,"MX") || checkdnsrr($domain,"A"))) { + $errors[] = 'Email address domain has no relevant DNS records.'; + return false; + } + return true; +} diff --git a/public_html/includes/navbar.php b/public_html/includes/navbar.php index f40d7a8..26db2f0 100644 --- a/public_html/includes/navbar.php +++ b/public_html/includes/navbar.php @@ -11,10 +11,10 @@ function get_menu_pages() { /* 'about' => [ 'name' => 'About', 'path' => '#' - ], + ],*/ 'join' => [ 'name' => 'Join', - 'path' => '#' - ], + 'path' => 'join.php' + ],/* 'links' => [ 'name' => 'Useful Links', 'path' => '#' ], diff --git a/public_html/includes/utils.php b/public_html/includes/utils.php index ee5bb6c..a9f096f 100644 --- a/public_html/includes/utils.php +++ b/public_html/includes/utils.php @@ -44,3 +44,14 @@ function cache_control_suffix($path) { $modified = date('Ymd-His', filemtime($doc_root . $site_root . $path)); return "?v=$modified"; } + +function show_error_list($errors) { + if (empty($errors)) return;?> + <div class="error"><?php + foreach ($errors as $error) {?><p><?php esc($error);?></p><?php }?> + </div><?php +} + +function key_to_human_text($key) { + return ucfirst(str_replace('_', ' ', $key)); +} diff --git a/public_html/join.php b/public_html/join.php new file mode 100644 index 0000000..ca1a2a5 --- /dev/null +++ b/public_html/join.php @@ -0,0 +1,161 @@ +<?php +require_once('includes/utils.php'); +require_once('includes/html-templating.php'); +require_once('includes/navbar.php'); +require_once('includes/fields.php'); +require_once('includes/form-validation.php'); +require_once('includes/database.php'); + +function validate_member_data(&$errors, $data) { + foreach (['first_name', 'surname', 'email_address'] as $field) + require_field($errors, $data, $field); + + if (empty($errors)) { + validate_email_address($errors, $data['email_address']); + //TODO: will want to turn this back on in the future. + /*if (record_exists('members', simple_where('email_address', $data['email_address']))) + $errors[] = 'Email address has already been used.';*/ + } + + return empty($errors); +} + +function confirm_sent_data($data) {?> + <form method="post" action=""> + <table style="width: max-content;"><?php + foreach (['first_name', 'surname', 'email_address', 'address_line_1', + 'address_line_2', 'city', 'region', 'postcode', 'country'] + as $key) { + if (!empty($data[$key])) {?> + <tr> + <th><?php esc(key_to_human_text($key));?>:</th> + <td><?php esc($data[$key]);?></td> + </tr><?php + hidden_field($key, $data[$key]); + } + }?> + </table> + <input type="submit" name="back" value="Back" /> + <input type="submit" name="paypal" value="Pay for membership now via PayPal" /> + <input type="submit" name="other-payment" value="Pay for membership later using another payment method" /> + </form><?php +} + +function store_member_data($data) { + $fields['date_added'] = date('Y-m-d H:i:s'); + foreach (['first_name', 'surname', 'email_address', 'address_line_1', + 'address_line_2', 'city', 'region', 'postcode', 'country', 'paypal_attempt'] + as $key) + if (!empty($data[$key])) $fields[$key] = $data[$key]; + + insert_array('members', $fields); +} + +function additional_stylesheets() { + stylesheet('fields'); +} + +function content() {?> + <h1>Join the YCRA</h1> + <?php + if (array_key_exists('paypal-cancel', $_GET)) {?> + <p>Your PayPal payment has been cancelled and you will not be charged..</p><?php + return; + } + if (array_key_exists('paypal-paid', $_GET)) {?> + <p>Thank you for paying for your YCRA membership via PayPal. Congratulations + on becoming a YCRA member!</p><?php + return; + } + $errors = $params = []; + + if (array_key_exists('back', $_POST)) + $params = $_POST; + else if (array_key_exists('join', $_POST)) { + if (validate_member_data($errors, $_POST)) {?> + <p>Please check that the information you entered (as shown below) is + correct.</p><?php + + confirm_sent_data($_POST); + return; + } + else $params = $_POST; + } + else if (array_key_exists('paypal', $_POST)) { + if (validate_member_data($errors, $_POST)) + $errors[] = 'Error occurred redirecting to PayPal. Please try again.'; + $params = $_POST; + } + else if (array_key_exists('other-payment', $_POST)) { + if (validate_member_data($errors, $_POST)) { + store_member_data($_POST);?> + <p>We have received your data. You will become a member of the YCRA after + you have paid the membership fee.</p><?php + return; + } + else $params = $_POST; + }?> + + <p>Please use the form below to either join the YCRA, or express interest in + joining the YCRA.</p> + + <p>You will become a member of the YCRA if you enter your details below and + then subsequently pay for membership.</p> + + <p>Please view our <a <?php href('privacy-policy.php');?>>privacy policy</a> + for information on how we will process your data.</p> + + <?php show_error_list($errors);?> + + <form method="post" action=""><?php + text_field($params, 'first_name', 'First name', ['required'=>'']); + text_field($params, 'surname', 'Surname', ['required'=>'']); + email_field($params, 'email_address', 'Email address', ['required'=>'']);?> + + <p>When you join the YCRA, we will send you a membership pack. If you do + <b>not</b> pay for your membership via PayPal, we will need your address. + Please either enter it here, or provide it when you pay for your + membership.</p><?php + + text_field($params, 'address_line_1', 'Address line 1'); + text_field($params, 'address_line_2', 'Address line 2'); + text_field($params, 'city', 'City'); + text_field($params, 'region', 'Region'); + text_field($params, 'postcode', 'Postcode'); + text_field($params, 'country', 'Country');?> + + <input type="submit" name="join" value="Join" /> + </form> + + <?php +} + + +if (array_key_exists('paypal', $_POST)) { + if (validate_member_data($errors, $_POST)) { + store_member_data(array_merge($_POST, ['paypal_attempt'=>1])); + $fields = [ 'cmd' => '_cart', + 'business' => 'treasurer@ycra.org.uk', + 'upload' => '1', + 'currency_code' => 'GBP', + 'item_name_1' => 'One year YCRA membership', + 'item_number_1' => 'YCRA-MEM-1', + 'quantity_1' => 1, + 'amount_1' => 15, + 'no_shipping' => 2, + 'no_note' => 1, + 'cancel_return' => 'https://ycra.org.uk/join.php?paypal-cancel=1', + 'return' => 'https://ycra.org.uk/join.php?paypal-paid=1', + ]; + $url_fields = []; + foreach ($fields as $field=>$value) + $url_fields[] = urlencode($field) . '=' . urlencode($value); + + $url = 'https://www.paypal.com/cgi-bin/webscr?' . implode('&', $url_fields); + header("Location: $url"); + return; + } +} + +require_once('includes/template.php'); +?> -- 2.25.1