From c40e831afc3a393763d3ddf7bb78ee65a13daf84 Mon Sep 17 00:00:00 2001
From: bill bittner <bittner.w@gmail.com>
Date: Thu, 4 Jan 2018 18:34:17 -0800
Subject: [PATCH] added validation to url input

---
 public/assets/js/generalFunctions.js      |  4 +-
 react/components/publishForm.jsx          | 14 +++-
 react/components/urlChooser.jsx           | 99 +++++++++++++++++++++++
 react/components/urlInput.jsx             | 39 ---------
 react/uploader.js                         | 74 ++++++++++++++---
 views/partials/publishForm-Url.handlebars | 16 ----
 6 files changed, 173 insertions(+), 73 deletions(-)
 create mode 100644 react/components/urlChooser.jsx
 delete mode 100644 react/components/urlInput.jsx

diff --git a/public/assets/js/generalFunctions.js b/public/assets/js/generalFunctions.js
index ec56009f..6ba5515b 100644
--- a/public/assets/js/generalFunctions.js
+++ b/public/assets/js/generalFunctions.js
@@ -7,7 +7,7 @@ function getRequest (url) {
             if (xhttp.readyState == 4 ) {
                 if ( xhttp.status == 200) {
                     resolve(xhttp.response);
-                } else if (xhttp.status == 401) {
+                } else if (xhttp.status == 403) {
                     reject('Wrong channel name or password');
                 } else {
                     reject('request failed with status:' + xhttp.status);
@@ -48,7 +48,7 @@ function toggleSection(event){
 	var slaveElement = document.getElementById(dataSet.slaveelementid);
 	var closedLabel = dataSet.closedlabel;
 	var openLabel = dataSet.openlabel;
-	
+
 	if (status === "false") {
 		slaveElement.hidden = false;
 		masterElement.innerText = openLabel;
diff --git a/react/components/publishForm.jsx b/react/components/publishForm.jsx
index 839c3c9a..93813475 100644
--- a/react/components/publishForm.jsx
+++ b/react/components/publishForm.jsx
@@ -2,7 +2,7 @@ import React from 'react';
 import PreviewDropzone from './previewDropzone.jsx';
 import TitleInput from './titleInput.jsx';
 import ChannelSelector from './channelSelector.jsx';
-import UrlInput from './urlInput.jsx';
+import UrlChooser from './urlChooser.jsx';
 import ThumbnailInput from './thumbnailInput.jsx';
 import MetadataInputs from './metadataInputs.jsx';
 
@@ -79,6 +79,16 @@ class PublishForm extends React.Component {
         <div className="column column--5 column--sml-10 align-content-top">
           <div id="publish-active-area" className="row row--padded">
 
+            <UrlChooser
+              fileName={this.props.file.name}
+              claim={this.props.claim}
+              publishToChannel={this.props.publishToChannel}
+              loggedInChannelName={this.props.loggedInChannelName}
+              loggedInChannelShortId={this.props.loggedInChannelShortId}
+              updateUploaderState={this.updateUploaderState}
+              makeGetRequest={this.props.makeGetRequest}
+            />
+
             <AnonymousOrChannelSelect publishToChannel={this.props.publishToChannel} updateUploaderState={this.props.updateUploaderState}/>
 
             <ChannelSelector
@@ -89,8 +99,6 @@ class PublishForm extends React.Component {
               channelError={this.state.channelError}
             />
 
-            <UrlInput file={this.props.file}/>
-
             { (this.props.file.type === 'video/mp4') && <ThumbnailInput thumbnail={this.props.thumbnail}/> }
 
             <MetadataInputs />
diff --git a/react/components/urlChooser.jsx b/react/components/urlChooser.jsx
new file mode 100644
index 00000000..b0cb0fee
--- /dev/null
+++ b/react/components/urlChooser.jsx
@@ -0,0 +1,99 @@
+import React from 'react';
+
+function UrlMiddle ({publishToChannel, loggedInChannelName, loggedInChannelShortId}) {
+  if (publishToChannel) {
+    if (loggedInChannelName) {
+      return <span id="url-channel" className="url-text--secondary">{loggedInChannelName}:{loggedInChannelShortId} /</span>;
+    }
+    return <span id="url-channel-placeholder" className="url-text--secondary tooltip">@channel<span
+          className="tooltip-text">Select a channel below</span> /</span>;
+  }
+  return (
+    <span id="url-no-channel-placeholder" className="url-text--secondary tooltip">xyz<span className="tooltip-text">This will be a random id</span> /</span>
+  );
+}
+
+class UrlInput extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {
+      urlError    : null,
+      urlBeginning: 'spee.ch',
+      urlMiddle   : null,
+    };
+    this.handleInput = this.handleInput.bind(this);
+    this.validateClaimName = this.validateClaimName.bind(this);
+    this.cleanseClaimName = this.cleanseClaimName.bind(this);
+    this.checkClaimIsValidAndAvailable = this.checkClaimIsValidAndAvailable.bind(this);
+  }
+  handleInput (event) {
+    event.preventDefault();
+    let value = event.target.value;
+    const name = event.target.name;
+    value = this.cleanseClaimName(value);
+    this.props.updateUploaderState(name, value);
+    this.checkClaimIsValidAndAvailable(value);
+  }
+  validateClaimName (claim) {
+    // ensure a name was entered
+    if (!claim || claim.length < 1) {
+      throw new Error('You must enter a name for your url');
+    }
+    // validate the characters in the 'name' field
+    const invalidCharacters = /[^A-Za-z0-9,-]/g.exec(claim);
+    if (invalidCharacters) {
+      throw new Error('"' + invalidCharacters + '" characters are not allowed');
+    }
+    return claim;
+  }
+  cleanseClaimName (name) {
+    name = name.replace(/\s+/g, '-'); // replace spaces with dashes
+    name = name.replace(/[^A-Za-z0-9-]/g, '');  // remove all characters that are not A-Z, a-z, 0-9, or '-'
+    return name;
+  }
+  checkClaimIsValidAndAvailable (claim) {
+    // validationFunctions.checkClaimName(event.target.value)
+    try {
+      claim = this.validateClaimName(claim);
+    } catch (error) {
+      this.setState({urlError: error.message});
+      return;
+    }
+    const that = this;
+    this.props.makeGetRequest(`/api/claim-is-available/${claim}`)
+      .then(() => {
+        that.setState({urlError: null});
+      })
+      .catch((error) => {
+        that.setState({urlError: error.message});
+      });
+  }
+  render () {
+    return (
+      <div>
+        <div className="row row--padded row--no-top row--wide">
+
+          <p id="input-error-claim-name" className="info-message-placeholder info-message--failure">{this.state.urlError}</p>
+
+          <div className="column column--3 column--sml-10">
+            <label className="label">URL:</label>
+          </div><div className="column column--7 column--sml-10 input-text--primary span--relative">
+
+            <span className="url-text--secondary">{this.state.urlBeginning} / </span>
+
+            <UrlMiddle publishToChannel={this.props.publishToChannel} loggedInChannelName={this.props.loggedInChannelName} loggedInChannelShortId={this.props.loggedInChannelShortId}/>
+
+            <input type="text" id="claim-name-input" className="input-text" name='claim' placeholder="your-url-here" onInput={this.handleInput} value={this.props.claim}/>
+            { (this.props.claim && !this.state.urlError) && (
+              <span id="input-success-claim-name" className="info-message--success span--absolute">{'\u2713'}</span>
+            )}
+
+          </div>
+
+        </div>
+      </div>
+    );
+  }
+}
+
+module.exports = UrlInput;
diff --git a/react/components/urlInput.jsx b/react/components/urlInput.jsx
deleted file mode 100644
index 9105b33b..00000000
--- a/react/components/urlInput.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-
-class UrlInput extends React.Component {
-  constructor (props) {
-    super(props);
-    this.updateUrl = this.updateUrl.bind(this);
-  }
-  updateUrl (selectedOption) {
-    const urlChannel = document.getElementById('url-channel');
-    const urlNoChannelPlaceholder = document.getElementById('url-no-channel-placeholder');
-    const urlChannelPlaceholder = document.getElementById('url-channel-placeholder');
-    if (selectedOption === 'new' || selectedOption === 'login' || selectedOption === ''){
-      urlChannel.hidden = true;
-      urlNoChannelPlaceholder.hidden = true;
-      urlChannelPlaceholder.hidden = false;
-    } else if (selectedOption === 'anonymous'){
-      urlChannel.hidden = true;
-      urlNoChannelPlaceholder.hidden = false;
-      urlChannelPlaceholder.hidden = true;
-    } else {
-      urlChannel.hidden = false;
-      // show channel and short id
-      const selectedChannel = getCookie('channel_name');
-      const shortChannelId = getCookie('short_channel_id');
-      urlChannel.innerText = `${selectedChannel}:${shortChannelId}`;
-      urlNoChannelPlaceholder.hidden = true;
-      urlChannelPlaceholder.hidden = true;
-    }
-  }
-  render () {
-    return (
-      <div>
-        <h3>url component</h3>
-      </div>
-    );
-  }
-}
-
-module.exports = UrlInput;
diff --git a/react/uploader.js b/react/uploader.js
index 4c980d6e..9115f0c3 100644
--- a/react/uploader.js
+++ b/react/uploader.js
@@ -8,17 +8,18 @@ const DROPZONE = 'DROPZONE';
 const DETAILS = 'DETAILS';
 const STATUS = 'STATUS';
 const initialState = {
-  showComponent   : DROPZONE,  // DROPZONE, DETAILS, or PUBLISHING
-  loggedInChannel : null,
-  publishToChannel: false,
-  file            : null,
-  title           : '',
-  channel         : null,
-  url             : '',
-  thumbnail       : '',
-  description     : '',
-  license         : '',
-  nsfw            : '',
+  showComponent         : DROPZONE,  // DROPZONE, DETAILS, or PUBLISHING
+  loggedInChannelName   : null,
+  loggedInChannelShortId: null,
+  publishToChannel      : false,
+  file                  : null,
+  title                 : '',
+  channel               : null,
+  claim                 : '',
+  thumbnail             : '',
+  description           : '',
+  license               : '',
+  nsfw                  : '',
 };
 
 class Uploader extends React.Component {
@@ -30,10 +31,16 @@ class Uploader extends React.Component {
     this.clearUploaderState = this.clearUploaderState.bind(this);
     this.showComponent = this.showComponent.bind(this);
     this.stageFileAndShowDetails = this.stageFileAndShowDetails.bind(this);
+    this.makeGetRequest = this.makeGetRequest.bind(this);
+    this.makePostRequest = this.makePostRequest.bind(this);
   }
   componentDidMount () {
     // check for whether a channel is logged in
     // if so, setState loggedInChannel to the channel name
+    // const loggedInChannel = getCookie('channel_name');
+    // this.setState({loggedInChannel})
+    // const loggedInChannelShortId = getCookie('short_channel_id');
+    // this.setState({loggedInChannelShortId})
   }
   updateUploaderState (name, value) {
     console.log(`updateUploaderState ${name} ${value}`);
@@ -52,6 +59,45 @@ class Uploader extends React.Component {
     // hide the dropzone and show the details
     this.showComponent(DETAILS);
   }
+  makeGetRequest (url) {
+    return new Promise((resolve, reject) => {
+      let xhttp = new XMLHttpRequest();
+      xhttp.open('GET', url, true);
+      xhttp.responseType = 'json';
+      xhttp.onreadystatechange = () => {
+        if (xhttp.readyState == 4 ) {
+          if ( xhttp.status == 200) {
+            resolve(xhttp.response);
+          } else if (xhttp.status == 403) {
+            reject('Wrong channel name or password');
+          } else {
+            reject('request failed with status:' + xhttp.status);
+          };
+        }
+      };
+      xhttp.send();
+    });
+  }
+  makePostRequest (url, params) {
+    return new Promise((resolve, reject) => {
+      let xhttp = new XMLHttpRequest();
+      xhttp.open('POST', url, true);
+      xhttp.responseType = 'json';
+      xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+      xhttp.onreadystatechange = () => {
+        if (xhttp.readyState == 4 ) {
+          if ( xhttp.status == 200) {
+            resolve(xhttp.response);
+          } else if (xhttp.status == 403) {
+            reject('Wrong channel name or password');
+          } else {
+            reject('request failed with status:' + xhttp.status);
+          };
+        }
+      };
+      xhttp.send(params);
+    });
+  }
   render () {
     return (
       <div className="row row--tall flex-container--column">
@@ -62,12 +108,14 @@ class Uploader extends React.Component {
           <PublishForm
             updateUploaderState={this.updateUploaderState}
             clearUploaderState={this.clearUploaderState}
-            loggedInChannel={this.state.loggedInChannel}
+            makeGetRequest={this.makeGetRequest}
+            loggedInChannelName={this.state.loggedInChannelName}
+            loggedInChannelShortId={this.state.loggedInChannelShortId}
             publishToChannel={this.state.publishToChannel}
             file={this.state.file}
             title={this.state.title}
             channel={this.state.channel}
-            url={this.state.url}
+            claim={this.state.claim}
             thumbnail={this.state.thumbnail}
             description={this.state.description}
             license={this.state.license}
diff --git a/views/partials/publishForm-Url.handlebars b/views/partials/publishForm-Url.handlebars
index d5200ae4..e69de29b 100644
--- a/views/partials/publishForm-Url.handlebars
+++ b/views/partials/publishForm-Url.handlebars
@@ -1,16 +0,0 @@
-<div class="row row--padded row--wide">
-    <!--error display-->
-    <p id="input-error-claim-name" class="info-message-placeholder info-message--failure"  hidden="true"></p>
-    <!--url selection-->
-    <div class="column column--3 column--sml-10">
-        <label class="label">URL:</label>
-    </div><div class="column column--7 column--sml-10 input-text--primary span--relative">
-            <span class="url-text--secondary">spee.ch /</span>
-            <span id="url-channel" class="url-text--secondary" {{#if user}}{{else}}hidden="true"{{/if}}>{{user.channelName}}:{{user.shortChannelId}}</span>
-            <span id="url-no-channel-placeholder" class="url-text--secondary tooltip" {{#if user}}hidden="true"{{else}}{{/if}}>xyz<span class="tooltip-text">This will be a random id</span></span>
-            <span id="url-channel-placeholder" class="url-text--secondary tooltip" hidden="true">@channel<span class="tooltip-text">Select a channel above</span></span> /
-            <input type="text" id="claim-name-input" class="input-text" placeholder="your-url-here" oninput="validationFunctions.checkClaimName(event.target.value)">
-            <span id="input-success-claim-name" class="info-message--success span--absolute"></span>
-    </div>
-
-</div>
\ No newline at end of file