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