1<?php
2/**
3 * EGroupware - eTemplate serverside URL widget
4 *
5 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
6 * @package etemplate
7 * @subpackage api
8 * @link http://www.egroupware.org
9 * @author Ralf Becker <RalfBecker@outdoor-training.de>
10 * @author Nathan Gray
11 * @copyright 2002-16 by RalfBecker@outdoor-training.de
12 * @copyright 2012 Nathan Gray
13 * @version $Id$
14 */
15
16namespace EGroupware\Api\Etemplate\Widget;
17
18use EGroupware\Api\Etemplate;
19
20/**
21 * eTemplate URL widget handles URLs, emails & phone numbers
22 */
23class Url extends Etemplate\Widget
24{
25	/**
26	 * Regexes for validating email addresses incl. email in angle-brackets eg.
27	 * + "Ralf Becker <rb@stylite.de>"
28	 * + "Ralf Becker (Stylite AG) <rb@stylite.de>"
29	 * + "<rb@stylite.de>" or "rb@stylite.de"
30	 * + '"Becker, Ralf" <rb@stylite.de>'
31	 * + "'Becker, Ralf' <rb@stylite.de>"
32	 * but NOT:
33	 * - "Becker, Ralf <rb@stylite.de>" (contains comma outside " or ' enclosed block)
34	 * - "Becker < Ralf <rb@stylite.de>" (contains <    ----------- " ---------------)
35	 * automatic cleaning of common mistakes (makes no sense to complain about them, as they are not visible to the user)
36	 * - "\u200Bfrancesca.klein@ikem.de" (starts with an unicode "zero width space")
37	 * - "info︃@joswieg.de" (Unicode variation selector "\uFE0X" before the @, meaning eg. not to be displayed as Emotji)
38	 *
39	 * About umlaut or IDN domains: we currently only allow German umlauts in domain part!
40	 * Client-side forbids all non-ascii chars in local part, as Horde does not yet support SMTPUTF8 extension (rfc6531)
41	 * and we get a "SMTP server does not support internationalized header data" error otherwise.
42	 * We can't do that easily on server-side, as used \x80-\xf0 works in JavaScript to detect non-ascii,
43	 * but gives an invalid utf-8 compilation error in PHP together with /u modifier.
44	 *
45	 * Same preg is in et2_widget_url Javascript class, but no \x00 allowed and /u modifier for utf8!
46	 */
47	const EMAIL_PREG = "/^(([^,<][^,<]+|\042[^\042]+\042|\'[^\']+\'|)\s?<)?[^\x01-\x20()\xe2\x80\x8b<>@,;:\042\[\]]+(?<![.\s])@([a-z0-9ÄÖÜäöüß](|[a-z0-9ÄÖÜäöüß_-]*[a-z0-9ÄÖÜäöüß])\.)+[a-z]{2,}>?$/iu";
48
49	// allow private IP addresses (starting with 10.|169.254.|192.168.) too
50	//const URL_PREG = '_^(?:(?:https?|ftp)://)?(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(localhost)|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$_iuS';
51	const URL_PREG = '_^(?:(?:https?|ftp)://)?(?:\S+(?::\S*)?@)?(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3})|(localhost)|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$_iuS';
52
53	/**
54	 * Validate input
55	 *
56	 * Following attributes get checked:
57	 * - needed: value must NOT be empty
58	 * - maxlength: maximum length of string (longer strings get truncated to allowed size)
59	 * - preg: perl regular expression incl. delimiters (set by default for int, float and colorpicker)
60	 * - int and float get casted to their type
61	 *
62	 * @param string $cname current namespace
63	 * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
64	 * @param array $content
65	 * @param array &$validated=array() validated content
66	 * @param array $expand=array values for keys 'c', 'row', 'c_', 'row_', 'cont'
67	 */
68	public function validate($cname, array $expand, array $content, &$validated=array())
69	{
70		$form_name = self::form_name($cname, $this->id, $expand);
71
72		if (!$this->is_readonly($cname, $form_name))
73		{
74			$value = $value_in = self::get_array($content, $form_name);
75
76			if ((string)$value === '' && $this->attrs['needed'])
77			{
78				self::set_validation_error($form_name,lang('Field must not be empty !!!'),'');
79				return;
80			}
81			elseif ((string)$value != '' && !isset($this->attrs['preg']))
82			{
83				$url_valid = true;
84				switch($this->type)
85				{
86					case 'url':
87						$this->attrs['preg'] = self::URL_PREG;
88						if($this->attrs['allow_path'])
89						{
90							$url_valid = $value[0] === '/';
91
92							// Just need to override the regex
93							$this->attrs['preg'] = '_\/.*_';
94						}
95						// if no protocol given eg. "www.egroupware.org" prepend "http://" for validation
96						else if (($missing_protocol = strpos($value, '://') === false))
97						{
98							$value = 'http://'.$value;
99						}
100						if($url_valid && !$this->attrs['allow_path'])
101						{
102							$url_valid = filter_var($value, FILTER_VALIDATE_URL) ||
103								// Remove intl chars & check again, but if it passes we'll keep the original
104								filter_var(preg_replace('/[^[:print:]]/','',$value), FILTER_VALIDATE_URL);
105						}
106						if(array_key_exists('trailing_slash', $this->attrs))
107						{
108							$trailing_slash = substr($value, -1) === '/';
109							$url_valid = (($this->attrs['trailing_slash'] == 'true') == $trailing_slash);
110						}
111						//error_log(__METHOD__."() filter_var(value=".array2string($value).", FILTER_VALIDATE_URL)=".array2string(filter_var($value, FILTER_VALIDATE_URL))." --> url_valid=".array2string($url_valid));
112						// remove http:// validation prefix again
113						if ($missing_protocol)
114						{
115							$value = substr($value, 7);
116						}
117						if (!$url_valid)
118						{
119							self::set_validation_error($form_name,lang("'%1' has an invalid format !!!",$value),'');
120							return;
121						}
122						break;
123					case 'url-email':
124						// some automatic cleaning: unicode variation selectors, zero width space
125						$value = preg_replace('/[\x{FE00}-\x{FE0F}\x{200B}]/u', '', $value);
126						$this->attrs['preg'] = self::EMAIL_PREG;
127						break;
128				}
129			}
130
131			$valid =& self::get_array($validated, $form_name, true);
132
133			if ((int) $this->attrs['maxlength'] > 0 && strlen($value) > (int) $this->attrs['maxlength'])
134			{
135				$value = substr($value,0,(int) $this->attrs['maxlength']);
136			}
137			if ($this->attrs['preg'] && !preg_match($this->attrs['preg'],$value))
138			{
139				switch($this->type)
140				{
141					default:
142						//error_log("preg_match('{$this->attrs['preg']}', '$value')=".array2string(preg_match($this->attrs['preg'], $value)));
143						self::set_validation_error($form_name,lang("'%1' has an invalid format !!!",$value)/*." !preg_match('$this->attrs[preg]', '$value')"*/,'');
144						break;
145				}
146			}
147			if (true) $valid = $value;
148			//error_log(__METHOD__."() $form_name: ".array2string($value_in).' --> '.array2string($value));
149		}
150	}
151
152	/**
153	 * Handle ajax searches for existing contact based on email
154	 *
155	 * @return boolean $_email exists or not
156	 */
157	public static function ajax_contact($_email)
158	{
159		$email = \EGroupware\Api\Mail::stripRFC822Addresses(array($_email));
160		$response = \EGroupware\Api\Json\Response::get();
161		$result = $GLOBALS['egw']->contacts->search(array('contact_email'=>$email[0], 'contact_email_home' => $email[0]), array('email','email_home'),
162			'', '', '', false, 'OR', false);
163		$response->data($result ? true : false);
164	}
165}
166