Allow purchasing of YCRA subscriptions.
authorSam White <webmaster@ycra.org.uk>
Fri, 10 Sep 2021 21:35:03 +0000 (21:35 +0000)
committerSam White <webmaster@ycra.org.uk>
Fri, 10 Sep 2021 21:35:03 +0000 (21:35 +0000)
config.php.sample
public_html/css/common.css
public_html/css/fields.css [new file with mode: 0644]
public_html/includes/database.php [new file with mode: 0644]
public_html/includes/fields.php [new file with mode: 0644]
public_html/includes/form-validation.php [new file with mode: 0644]
public_html/includes/navbar.php
public_html/includes/utils.php
public_html/join.php [new file with mode: 0644]

index 29381f27cb421f233f9301e8d1a0b27e7a54129d..e060c36f3f0ba434cb12a1f3fb99cf3c527a7b5a 100644 (file)
@@ -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",
   ];
 }
index 754bd9b5823af2f60f5ad28d723a52ac1a04a20e..0aa5089397720a50a4e6856b49d390ed5ec23dc3 100644 (file)
@@ -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 (file)
index 0000000..66cb445
--- /dev/null
@@ -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 (file)
index 0000000..4aa870f
--- /dev/null
@@ -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 (file)
index 0000000..2419528
--- /dev/null
@@ -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 (file)
index 0000000..43ab85e
--- /dev/null
@@ -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;
+}
index f40d7a8b14cf3c31db09a35f0b74c6495384ee8b..26db2f0978003c71911c3219e368d3a380fe7789 100644 (file)
@@ -11,10 +11,10 @@ function get_menu_pages() {
     /*
     'about' => [ 'name' => 'About',
                 'path' => '#'
-              ],
+                ],*/
     'join' => [ 'name' => 'Join',
-                'path' => '#'
-              ],
+                'path' => 'join.php'
+              ],/*
     'links' => [ 'name' => 'Useful Links',
                 'path' => '#'
               ],
index ee5bb6c9fe0ef007bf6473b55806d5a9d5ba61fc..a9f096ff0bde8510056c3684a0989938f358c505 100644 (file)
@@ -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 (file)
index 0000000..ca1a2a5
--- /dev/null
@@ -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');
+?>