<?php namespace ProcessWire;

/**
 * An Inputfield for handling email addresses
 * 
 * ProcessWire 3.x, Copyright 2023 by Ryan Cramer
 * https://processwire.com
 *
 * @property int $confirm Specify 1 to make it include a second input for confirmation
 * @property string $confirmLabel label to accompany second input
 * @property int maxlength Max length of email address (default=512)
 * @property int|bool $allowIDN Allow IDN emails? 1=yes for domain, 2=yes for domain+local part (default=0) 3.0.212+
 * 
 * @method string renderConfirm(array $attrs)
 *
 */
class InputfieldEmail extends InputfieldText {

	public static function getModuleInfo() {
		return array(
			'title' => __('Email', __FILE__), // Module Title
			'version' => 102,
			'summary' => __('E-Mail address in valid format', __FILE__) // Module Summary
		);
	}

	/**
	 * Construct
	 * 
	 */
	public function __construct() {
		$this->setAttribute('name', 'email'); 
		parent::__construct();
		$this->setAttribute('type', 'email'); 
		$this->setAttribute('maxlength', 250); 
		$this->setAttribute('size', 0); 
		$this->set('confirm', 0); // when 1, two inputs will appear and both must match
		$this->set('confirmLabel', $this->_('Confirm')); 
		$this->set('value2', '');
		$this->set('allowIDN', 0);
	}

	/**
	 * Render input
	 * 
	 * @return string
	 * 
	 */
	public function ___render() {
		
		if(!$this->label || $this->label === $this->attr('name')) {
			$this->label = $this->_('E-Mail'); // label headline when no default specified
		}
		
		if($this->confirm && count($this->getErrors())) $this->val('');
		
		$attrs = $this->getAttributes();
		if((int) $this->allowIDN > 1) {
			// UTF-8 emails are not supported by HTML5 email input type at least in current Chrome
			$attrs['pattern'] = '[^@]+@[^\.]+\...+';
			$attrs['type'] = 'text';
		}
		
		$out = "<input " . $this->getAttributesString($attrs) . " />"; 
		
		if($this->confirm) $out .= $this->renderConfirm($attrs); 
		
		return $out; 
	}

	/**
	 * Render the secondary “Confirm” email input 
	 * 
	 * @param array $attrs
	 * @return string
	 * 
	 */
	protected function ___renderConfirm(array $attrs) {
		
		foreach(array('id', 'name') as $key) {
			if(isset($attrs[$key])) $attrs[$key] = '_' . $attrs[$key] . '_confirm';
		}
		
		$attrs['aria-label'] = $this->confirmLabel;
		$attrs['placeholder'] = $this->confirmLabel;
		
		return "<div style='margin-top:0.5em'><input " . $this->getAttributesString($attrs) . " /></div>";
	}

	/**
	 * Set attribute value
	 * 
	 * @param string $value
	 * @return string
	 * 
	 */
	protected function setAttributeValue($value) {
		$value = (string) $value;
		if(strlen($value)) { 
			$value = $this->sanitizeEmail($value); 	
			if(!strlen($value)) {
				$this->error($this->_('Please enter a valid e-mail address')); // Error message when email address is invalid
			}
		} else {
			$value = '';
		}
		return $value; 
	}

	/**
	 * Process input 
	 * 
	 * @param WireInputData $input
	 * @return $this
	 * 
	 */
	public function ___processInput(WireInputData $input) {

		$sanitizer = $this->wire()->sanitizer;
		$textTools = $sanitizer->getTextTools();
		$field = $this->hasField;
		$fieldtype = $field ? $field->type : $this->hasFieldtype; /** @var FieldtypeEmail $fieldtype */
		$page = $this->hasPage;
		$errors = array();
		$valuePrevious = $this->val();
		$name = $this->attr('name');
		$idnError = false;
		
		parent::___processInput($input);
		
		$value = $this->val();
		$changed = $textTools->strtolower($value) !== $textTools->strtolower($valuePrevious);
		
		if($this->confirm) {
			$value2 = $this->sanitizeEmail($input["_{$name}_confirm"]);
			if((strlen($value) || strlen($value2)) && strtolower($value) !== strtolower($value2)) {
				$errors[] = $this->_('The emails you entered did not match, please enter again');
			}
		}
		
		if($changed && $value && $field && $page && $field->hasFlag(Field::flagUnique)) {
			$fields = $this->wire()->fields;
			$pageId = $fields->tableTools()->valueExists($field, $value);
			if($pageId && $pageId != $page->id) {
				$errors[] = sprintf($this->_('Email “%s” is already in use, please use a different one'), $value);
			}
		}
		
		if($changed && !$this->allowIDN && strpos($value, 'xn-') !== false) {
			$idnError = true;
		}
		
		if($changed && empty($value) && $this->allowIDN < 2 && $input->$name) {
			// value was made empty by sanitization, see if it is because of IDN conversion
			$inputValue1 = $textTools->strtolower($input->$name); // value as input
			$inputValue2 = $sanitizer->email($inputValue1, array('allowIDN' => 2)); // input value sanitized
			if($inputValue1 && $inputValue1 === $inputValue2) $idnError = true;
			unset($inputValue1, $inputValue2);
		}
		
		if($idnError) {
			if($this->allowIDN) {
				$errors[] = $this->_('Email with extended characters in the local-part of local-part@domain.com is not enabled'); 
			} else {
				$errors[] = $this->_('Internationalized domain name (IDN) emails are not enabled, please use a non-IDN email');
			}
		}
		
		if($fieldtype instanceof FieldtypeEmail) {
			$max = $fieldtype->getMaxEmailLength();
			if(strlen($value) > $max) {
				$errors[] = sprintf($this->_('Email exceeded max allowed length of %d characters'), $max);
			}
		}

		if(count($errors)) {
			foreach($errors as $error) $this->error($error);
			$this->val($valuePrevious);
		}

		return $this;
	}

	/**
	 * Sanitize email address
	 * 
	 * @param string $email
	 * @return string
	 * 
	 */
	protected function sanitizeEmail($email) {
		return $this->wire()->sanitizer->email($email, array(
			'allowIDN' => (int) $this->allowIDN,
		));
	}

	/**
	 * Field config
	 * 
	 * @return InputfieldWrapper
	 * 
	 */
	public function ___getConfigInputfields() {
		
		$inputfields = parent::___getConfigInputfields();
		$skips = array('stripTags', 'pattern'); 
		
		foreach($skips as $name) {
			$f = $inputfields->get($name);
			if($f) $inputfields->remove($f);
		}

		$f = $inputfields->InputfieldCheckbox;
		$f->attr('name', 'confirm');
		$f->label = $this->_('Confirm email address?');
		$f->description = $this->_('When checked, two email inputs will appear and the user will have to enter their email address twice to confirm it. This helps reduce the possibility of typos.');
		$f->attr('value', 1);
		$f->collapsed = $this->confirm ? Inputfield::collapsedNo : Inputfield::collapsedYes; 
		if($this->confirm) $f->attr('checked', 'checked');
		$inputfields->add($f);
	
		if($this->hasField && $this->hasField->name === 'email') {
			// do not allow IDN for ProcessWire's system email field
		} else {
			$f = $inputfields->InputfieldRadios;
			$f->attr('name', 'allowIDN');
			$f->label = $this->_('Allow internationalized domain name (IDN) emails?');
			$f->description =
				$this->_('Please note that not all email systems support sending/receiving emails that contain IDNs and/or UTF-8 characters.') . ' ' . 
				$this->_('Choose ASCII standard emails for broadest compatibility.');
			$f->notes =
				$this->_('Use ASCII standard if the email address is used for any kind of authentication or login.') . ' ' .
				$this->_('We also recommend ASCII standard emails if you will be sending messages to the email address.');
			$f->addOption(0,
				$this->_('Use ASCII standard emails') . ' ' .
				'`' . $this->_('bob@domain.com') . '` ' .
				'[span.detail] ' . $this->_('(recommended)') . ' [/span]'
			);
			$f->addOption(1,
				$this->_('Allow IDN host/domain') . ' ' .
				'`' . $this->_('bob@dømain.com') . '`'
			);
			$f->addOption(2,
				$this->_('Allow UTF-8 local-part and IDN host/domain') . ' ' .
				'`' . $this->_('bøb@dømain.com') . '`'
			);
			$f->val($this->allowIDN);
			$f->collapsed = !$this->allowIDN;
			$inputfields->add($f);
		}
		
		return $inputfields;	
	}

}
