aboutsummaryrefslogblamecommitdiffstats
path: root/Zotlabs/Module/Invite.php
blob: 24792c8c1ce53caa64c773ed37e5c17c542e4e36 (plain) (tree)
1
2
3
4
5
6
7
8
9


                         



                           
   
                        





                                                
                                 
 



























                                                                                                                     
                         

                        
        

                                                                            

                               
 

                                                                                                       

                               

                                                                            







                                                                                   
                               
                 












                                                                                                  
        

















































































                                                                                                                                                   
                         









































                                                                                                                           
        


                                                           
        
























                                                                                                                 


                                                           


                                                                                                              

                                 
        

































                                                                                                                                                                   
                         

                                                                                                                   
                 





                                                                                                             



                       
                        

                       

                                       
                                                                             

                               
 



                                                                             






                                                                                                            


                                  
















































                                                                                                                          
                                           
        























                                                                                                                       
                                                         




























                                                                                                                    
        












                                                                                                                    
                         


                                                                                               
                 




















































                                                                                                                             
                                  
















                                                                                                                  
                         









                                                                                     

                                                                                         

                                                         
                                                           














                                                                                                                              

                                                
                

                          
















                                                                                                                               
 
 
<?php
namespace Zotlabs\Module;

use App;
use Zotlabs\Lib\Apps;
use Zotlabs\Web\Controller;

/**
 * module: invitexv2.php
 *
 * send email invitations to join social network
 *
 */


class Invite extends Controller {

	/**
	 * While coding this, I want to introduce a system of qualified messages and notifications.
	 * Each message consists of a 3 letter prefix, a 4 digit number and a one letter suffix (PREnnnnS).
	 * The spirit about is not from me, but many decades used by IBM inc. in devel with best success. 
	 *
	 * The system prefix, used uppercase as system message id, lowercase as css and js prefix (classes, ids etc).
	 * Usually not used as self::MYP, but placed in the code dominant enough for easy to find.
	 *
	 * Concrete here:
	 * The prefix indicates Z for the Zlabs(core), A for Account stuff, I for Invite.
	 * The numbers scope will be 00xx within/for templates, 01xx for get, 02xx for post functions.
	 * Message qualification ends with a uppercase suffix, where
	 * 	I=Info(only), 
	 *	W=Warning(more then info and less then error),
	 *	E=Error, 
	 *	F=Fatal(for unexpected errors).
	 * Btw, in case of using fail2ban, a scan of messages going to log is very much more with ease, 
	 * esspecially in multi language driven systems where messages vary.
	 *
	 * @author Hilmar Runge
	 * @version 2.0.0
	 * @since 2020-01-20
	 *
	 */

	const MYP = 	'ZAI';
	const VERSION =	'2.0.0';

	function post() {

		// zai02
	
		if (! local_channel()) {
			notice( 'ZAI0201E,' .t('Permission denied.') . EOL);
			return;
		}

		if (! Apps::system_app_installed(local_channel(), 'Invite')) {
			notice( 'ZAI0202E,' . t('Invite App') . ' (' . t('Not Installed') . ')' . EOL);
			return;
		}
	
		check_form_security_token_redirectOnErr('/', 'send_invite');

		$ok = $ko = 0;
		$feedbk = '';
		$isajax = is_ajax();
		$eol 	= $isajax ? "\n" : EOL;
		$policy  = intval(get_config('system','register_policy'));
		if ($policy == REGISTER_CLOSED) {
			notice( 'ZAI0212E,' . t('Register is closed') . ')' . EOL);
			return;
		}
		if ($policy == REGISTER_OPEN)
			$flags = 0;
		elseif ($policy == REGISTER_APPROVE)
			$flags = ACCOUNT_PENDING;
		$flags  = ($flags | intval(get_config('system','verify_email')));

		// how many max recipients in one mail submit
		$maxto = get_config('system','invitation_max_recipients', 'na');
		If (is_site_admin()) {
			// set, if admin is operator, default to 12
			if ($maxto === 'na') set_config('system','invitation_max_recipients', 12);
		}
		$maxto = ($maxto === 'na') ? 12 : $maxto;
	
		// language code current for the invitation
		$lcc  = x($_POST['zailcc'])  && preg_match('/[a-z\-]{2,5}/', $_POST['zailcc'])  
				? $_POST['zailcc'] 
				: '';

		// expiration duration amount quantity, in case of doubts defaults 2
		$durn = x($_POST['zaiexpiren']) && preg_match('/[0-9]{1,2}/', $_POST['zaiexpiren']) 
				? trim(intval($_POST['zaiexpiren'])) 
				: '2';
		!$durn ? $durn = 2 : '';

		// expiration duration unit 1st letter (day, weeks, months, years), defaults days
		$durq = x($_POST['zaiexpire']) && preg_match('/[ihd]{1,1}/', $_POST['zaiexpire']) 
				? $_POST['zaiexpire']
				: 'd';

		$dur = self::calcdue($durn.$durq);
		$due = t('Note, the invitation code is valid up to') . ' ' . $dur['due'];

		if ($isajax) {
			$feedbk .= 'ZAI0207I ' . $due . $eol;
		}

		// take the received email addresses and discart duplicates
		$recips  = array_filter( array_unique( preg_replace('/^\s*$/', '', 
			((x($_POST,'zaito')) ? explode( "\n",$_POST['zaito']) : array() ) )));

		$havto = count($recips);

		if ( $havto > $maxto) {
			$feedbk .= 'ZAI0210E ' . sprintf( t('Too many recipients for one invitation (max %d)'), $maxto) . $eol;
			$ko++;
		
		} elseif ( $havto == 0 ) {
			$feedbk .= 'ZAI0211E ' . t('No recipients for this invitation') . $eol;
			$ko++;

		} else {

			// each email address
			foreach($recips as $n => $recip) {

				// if empty ignore
				$recip = $recips[$n] = trim($recip);
				if(! $recip) continue;

				// see if we have an email address who@domain.tld
				if (!preg_match('/^.{2,64}\@[a-z0-9.-]{4,32}\.[a-z]{2,12}$/', $recip)) {
					$feedbk .= 'ZAI0203E ' . ($n+1) . ': ' . sprintf( t('(%s) : Not a valid email address'), $recip) . $eol;
					$ko++;
					continue;
				}
				if(! validate_email($recip)) {
					$feedbk .= 'ZAI0204E ' . ($n+1) . ': ' . sprintf( t('(%s) : Not a real email address'), $recip) . $eol;
					$ko++;
					continue;
				}

				// do we accept the email (not black listed)
				if(! allowed_email($recip)) {
					$feedbk .= 'ZAI0205E ' . ($n+1) . ': ' . sprintf( t('(%s) : Not allowed email address'), $recip) . $eol;
					$ko++;
					continue;
				}

				// is the email address just in use for account or registered before
				$r = q("SELECT account_email       AS em FROM account  WHERE account_email = '%s'"
					  . " UNION "
					  ."SELECT reg_email           AS em FROM register WHERE reg_vital = 1 AND reg_email = '%s' LIMIT 1;",
						dbesc($recip),
						dbesc($recip)
				);
				if($r && $r[0]['em'] == $recip) {
					$feedbk .= 'ZAI0206E ' . ($n+1) . ': ' . sprintf( t('(%s) : email address already in use'), $recip) . $eol;
					$ko++;
					continue;
				}
		
				if ($isajax) {
					// seems we have an email address acceptable
					$feedbk .= 'ZAI0209I ' . ($n+1) . ': ' . sprintf( t('(%s) : Accepted email address'), $recip) . $eol;
				}
			}
		}

		if ($isajax) {
			// we are not silent on the ajax road
			echo json_encode(array('feedbk' => $feedbk, 'due' => $due));

			// that mission is complete
			killme();
			exit;
		}
		
		//	Total ?todo notice( t('Invitation limit exceeded. Please contact your site administrator.') . EOL);

		// any errors up to now in fg?


		// down from here, only on the main road (no more ajax)

		// tell if sth is to tell
		$feedbk ? notice($feedbk) . $eol : '';

		if ($ko > 0) return;

		// the personal mailtext
		$mailtext = ((x($_POST,'zaitxt'))    ? notags(trim($_POST['zaitxt']))    : '');

		// to log in db
		$reonar = json_decode( ((x($_POST,'zaireon'))  ? notags(trim($_POST['zaireon']))    : ''), TRUE, 8) ;

		// me, the invitor
		$account = App::get_account();
		$reonar['from'] = $account['account_email'];
		$reonar['date'] = datetime_convert();
		$reonar['fromip'] = $_SERVER['REMOTE_ADDR'];

		// who is the invitor on
		$inby = local_channel();

		$ok = $ko = 0;

		// send the mail(s)
		foreach($recips as $n => $recip) {
	
			$reonar['due'] = $due;
			$reonar['to']  = $recip;
			$reonar['txtpersonal'] = $mailtext;
	
			// generate an invide code to store and pm
			$invite_code = autoname(8) . rand(1000,9999);

			// again the final localized templates  $reonar['subject'] $reonar['lang'] $reonar['tpl']

			// save current operators lc and take the desired to mail
			push_lang($reonar['lang']);
			// resolve
			$tx = replace_macros(get_intltext_template('invite.'.$reonar['tpl'].'.tpl'), 
				array(
					'$projectname'		=> t('$Projectname'),
					'$invite_code'		=> $invite_code,
					'$invite_where' 	=> z_root() . '/register',
					'$invite_whereami'	=> str_replace('@', '@+', $reonar['whereami']),
					'$invite_whoami'	=> z_root() . '/channel/' . $reonar['whoami'],
					'$invite_anywhere'	=> z_root() . '/pubsites'
				)
			);
			// restore lc to operator
			pop_lang();

			$reonar['txttemplate'] = $tx;

			// pm
			$zem = z_mail(
				[ 
				'toEmail'        => $recip,
				'fromName'       => ' ',
				'fromEmail'      => $reonar['from'],
				'messageSubject' => $reonar['subject'],
				'textVersion'    => ($mailtext ? $mailtext . "\n\n" : '') . $tx . "\n" . $due,
				]
			);
	
			if(!$zem) {

				$ko++;
				$msg = 'ZAI0208E,' . sprintf( t('%s : Message delivery failed.'), $recip);

			} else {

				$ok++;
				$msg = 'ZAI0208I ' . sprintf( t('To %s : Message delivery success.'), $recip);

				// if verify_email is the rule, email becomes a dId2 - NO
				// $did2 = ($flags & ACCOUNT_UNVERIFIED) == ACCOUNT_UNVERIFIED ? $recip : '';
				
				// always enforce verify email with invitations, thus email becomes a dId2
				$did2 = $recip;
				$flags |= ACCOUNT_UNVERIFIED;

				// defaults vital, reg_pass
				$r = q("INSERT INTO register ("
				. "reg_flags,reg_didx,reg_did2,reg_hash,reg_created,reg_startup,reg_expires,reg_email,reg_byc,reg_uid,reg_atip,reg_lang,reg_stuff)"
				. " VALUES ( %d, 'i', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', '%s') ",
					intval($flags),
					dbesc($did2),
					dbesc($invite_code),
					dbesc(datetime_convert()),
					dbesc(datetime_convert()),
					dbesc($dur['due']),
					dbesc($recip),
					intval($inby),
					intval($account['account_id']),
					dbesc($reonar['fromip']),
					dbesc($reonar['lang']),
					dbesc(json_encode( array('reon' => $reonar) ))
				);	
			}
			$msg .= ' (a' . $account['account_id'] . ', c' . $inby . ', from:' . $reonar['from'] . ')';
			zar_log( $msg);
		}

		$ok + $ko > 0 
		? notice( 'ZAI0212I ' . sprintf( t('%1$d mail(s) sent, %2$d mail error(s)'), $ok, $ko) . EOL)
		: '';
		//logger( print_r( $reonar, true) );	

		return;
	}
	
	
	function get() {

		// zai1
	
		if(! local_channel()) {
			notice( 'ZAI0101E,' . t('Permission denied.') . EOL);
			return;
		}

		if(! Apps::system_app_installed(local_channel(), 'Invite')) {
			//Do not display any associated widgets at this point
			App::$pdl = '';

			$o = 'ZAI0102E,' . t('Invite App') . ' (' . t('Not Installed') . ')' . EOL;
			return $o;
		}

		if (! (get_config('system','invitation_also') || get_config('system','invitation_only')) ) {
			$o = 'ZAI0103E,' . t('Invites not proposed by configuration') . '. ';
			$o .= t('Contact the site admin');
			return $o;
		}

		// invitation_by_user may still not configured, the default 'na' will tell this
		// if configured, 0 disables invitations by users, other numbers are how many invites a user may propagate
		$invuser = get_config('system','invitation_by_user', 'na');
		
		// if the mortal user drives the invitation
		If (! is_site_admin()) {

			// when not configured, 4 is the default
			$invuser = ($invuser === 'na') ? 4 : $invuser;

			// a config value 0 disables invitation by users
			if (!$invuser) {
				$o = 'ZAI0104E, ' . t('Invites by users not enabled') . '. ';
				return $o;
			}

			if ($ihave >= $invuser) {
				notice( 'ZAI0105W,' . t('You have no more invitations available') . EOL);
				return '';
			}

		} else {
			// general deity admin invite limit infinite (theoretical)
			if ($invuser === 'na') set_config('system','invitation_by_user', 4);
			// for display only
			$invuser = '∞';
		}

		// xchan record of the page observer
		// while quoting matters the user, the sending is associated with a channel (of the user)
		// also the admin may and should decide, which channel will told to the public
		$ob = App::get_observer();
		if(! $ob)
			return 'ZAI0109F,' . t('Not on xchan') . EOL;
		$whereami = $ob['xchan_addr'];
		$channel  = App::get_channel();
		$whoami = $channel['channel_address'];

		// to pass also to post()
		$tao = 'tao.zai.whereami = ' . "'" . $whereami . "';\n"
			 . 'tao.zai.whoami = ' 	 . "'" . $whoami . "';\n";

		// expirations, duration interval
		$dur = self::calcdue();
		$tao .= 'tao.zai.expire = { durn: ' . $dur['durn'] 
			  					. ', durq: ' . "'" . $dur['durq']  . "'" 
			  					. ', due: '  . "'" . $dur['due']   . "' };\n";

		// to easy redisplay the empty form 
		nav_set_selected('Invite');
	
		// inform about the count of invitations we have at all
		$r = q("SELECT count(reg_id) as ct FROM register WHERE reg_vital = 1");		// where not admin TODO
		$wehave = ($r ? $r[0]['ct'] : 0);

		// invites max for all users except admins
		$invmaxau = intval(get_config('system','invitations_max_users'));
		if(! $invmaxau) {
			$invmaxau = 50;
			if (is_site_admin()) {
				set_config('system','invitations_max_users',intval($invmaxau));
			}
		}

		if ($wehave > $invmaxau) {
			if (! is_site_admin()) {
				$feedbk .= 'ZAI0200E,' . t('All users invitation limit exceeded.') . $eol;
			}
		}

		// let see how many invites currently used by the user
		$r = q("SELECT count(reg_id) AS n FROM register WHERE reg_vital = 1 AND reg_byc = %d",
			 intval(local_channel()));
		$ihave = $r ? $r[0]['n'] : 0;

		$tpl = get_markup_template('invite.tpl');

		$inv_rabots = array(
 					'i'	=> t('Minute(s)'),
 					'h' => t('Hour(s)')  ,
 					'd' => t('Day(s)')
		);
		$inv_expire = replace_macros(get_markup_template('field_duration.qmc.tpl'),
			 array(	
			 	'label'  	=> t('Invitation expires after'),
			 	'qmc'	 	=> 'zai', 
				'qmcid'		=> 'ZAI0014I',
			 	'field' => 	array(
			 		'name'  => 'expire', 
			 		'title' => t('duration up from now'),
			 		'value' => ($invexpire_n ? $invexpire_n : 2), 
			 		'min'  => '1', 
			 		'max'  => '99',
			 		'size' => '2',
					'default' => ($invexpire_u ? $invexpire_u : 'd')
			 	),
			 	'rabot'	=> 	$inv_rabots 
			 )
		);

		// let generate an invite code that here and never will be applied (only to fill displayed template)
		// real invite codes become generated for each recipient when we store the new invitation(s)
		// $invite_code = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz'), 0, 8) . rand(1000,9999);
		// let take one descriptive for template (as said is never used)
		$invite_code = 'INVITATE2020';
	
		// what languages we use now
		$lccmy	= ((isset(App::$config['system']['language'])) ? App::$config['system']['language'] : 'en');
		// and all the localized templates belonging to invite
		$tpls	= glob('view/*/invite.*.tpl');

		$tpla=$tplx=$tplxs=array();
		foreach ($tpls as $tpli) {
			list( $nop, $l, $t ) = explode( '/', $tpli);
			if ( preg_match('/\.subject/', $t) =='1' ) {
				// indicate a subject tpl exists
				$t=str_replace(array('invite.', '.subject', '.tpl'), '', $t);
				$tplxs[$l][$t]=true;
				continue; 
			}
			// collect unique template names cross all languages and
			// tpla[language][]=template those available in each language
			$tplx[] = $tpla[$l][] = str_replace( array('invite.', '.tpl'), '', $t);
		}
		asort( $langs = array_keys($tpla) );
		asort( $tplx = array_unique($tplx) );
 
		// prepare current language and the default standard template (causual) for js
		// With and in js, I use a var 'tao' as a shortcut for top array object
		// and also qualify the object with the prefix zai = tao.zai as my var used outsite functions
		// can be unique within the overall included spaghette whirls
		// one can say Im too lazy to write prototypes and just I can agree.
		// tao simply applies the fact of using the same var as object and/or array in ja.
		$tao.='tao.zai.lccmy = ' . "'" . $lccmy . "';\n" . 'tao.zai.itpl = ' . "'" . 'casual' . "';\n";

		$lcclane=$tx=$tplin='';
		//$lccsym='<span class="fa zai_fa zai_lccsym"></span>';	// alt 
		$tplsym='<span class="fa zai_fa"></span>';

		// I will uncomment for js console debug
		// $tao.='tao.zai.debug = ' . "'" . json_encode($tplxs) . "';\n";
		
		// running thru the localized templates (subjects and textmsgs) and bring them to tao
		// lcc LanguageCountryCode,
		// lcc2 is a 2 character and lcc5 a 5 character LanguageCountryCode
		foreach($tpla as $l => $tn) {

			// restyle lc to iso getttext format to avoid errors in js, hilite the current
			$lcc = str_replace('-', '_', $l);
			$hi = ($l == $lccmy) ? ' zai_hi' : '';
			$lcc2 = strlen($l) == 2 ? ' zai_lcc2' : '';
			$lcc5 = strlen($l) == 5 ? ' zai_lcc5' : '';
			$lccg = ' zai_lccg' . substr( $l, 0, 2 );
			$lcclane 
			.= 	'<span class="fa zai_fa zai_lccsym' . $lcc2 . $lcc5 . $lccg . '"></span>' 
			.	'<a href="javascript:;" class="zai_lcc' . $lcc2 . $lcc5 . $lccg . $hi . '">' . $lcc . '</a>';
			//  textmsg
			$tao .= 'tao.zai.t.' . $lcc . ' = {};' . "\n";
			//  subject
			$tao .= 'tao.zai.s.' . $lcc . ' = {};' . "\n";

			// resolve localized templates and take intented lc for
			foreach($tn as $t1) {

				// save current lc and take the desired
				push_lang($l);

				// resolve
				$tx = replace_macros(get_intltext_template('invite.'.$t1.'.tpl'), 
					array(
						'$projectname'		=> t('$Projectname'),
						'$invite_code'		=> $invite_code,
						'$invite_where' 	=> z_root() . '/register',
						'$invite_whereami'	=> $whereami,
						'$invite_whoami'	=> z_root() . '/channel/' . $whoami,
						'$invite_anywhere'	=> z_root() . '/pubsites'
					)
				);

				// a default subject if no associated exists
				$ts=t('Invitation');
				if ( $tplxs[$l][$t1] )
					$ts = replace_macros(get_intltext_template('invite.'.$t1.'.subject.tpl'), 
					array(
						'$projectname'		=> t('$Projectname'),
						'$invite_loc'		=> get_config('system','sitename')
					)
				);

				// restore lc to current foreground
				pop_lang();

				// bring to tao as js like it
				$tao .= 'tao.zai.t.' . $lcc . '.' . $t1 . " = '" . rawurlencode($tx) . "';\n"; 
				$tao .= 'tao.zai.s.' . $lcc . '.' . $t1 . " = '" . rawurlencode($ts) . "';\n"; 
			}
		}

		// hilite the current defauls just from the beginning
		foreach ($tplx as $t1) {
			$hi = ($t1 == 'casual') ? ' zai_hi' : '';
			$tplin .= $tplsym.'<a href="javascript:;" id="zai-' . $t1
					. '" class="invites'.$hi.'">' . $t1 . '</a>';
		}
		
		// fill the form for foreground
		$o = replace_macros($tpl, array(
			'$form_security_token' => get_form_security_token("send_invite"),
			'$zai'	=> strtolower(self::MYP),
			'$tao'	=> $tao,
			'$invite' => t('Send invitations'),
			'$ihave'  => 'ZAI0106I, ' . t('Invitations I am using') . ': ' . $ihave . ' / ' . $invuser,
			'$wehave' => 'ZAI0107I, ' . t('Invitations we are using') . ': ' . $wehave . ' / ' . $invmaxau,
			'$n10' => 'ZAI0010I',	'$m10' => t('§ Note, the email(s) sent will be recorded in the system logs'),
			'$n11' => 'ZAI0011I',	'$m11' => t('Enter email addresses, one per line:'),
			'$n12' => 'ZAI0012I',	'$m12' => t('Your message:'),
			'$n13' => 'ZAI0013I',	'$m13' => t('Invite template'),
			'$inv_expire' => $inv_expire,
			'$subject_label' => t('Subject:'),
			'$subject'	=> t('Invitation'),
			'$lcclane'		=> $lcclane,
			'$tplin'	=> $tplin,
			'$standard_message' => '',
			'$personal_message' => '',
			'$personal_pointer' => t('Here you may enter personal notes to the recipient(s)'),
			'$due' => t('Note, the invitation code is valid up to') . ' ' . $dur['due'],
			'$submit' => t('Submit')
		));
		
		return $o;
	}

	function calcdue($duri=false) {
		// expirations, duration interval
		if ($duri===false)
			$duri = get_config('system','register_expire', '2d');
		if ( preg_match( '/^[0-9]{1,2}[ihdwmy]{1}$/', $duri ) ) {
			$durq = substr($duri, -1);
			$durn = substr($duri, 0, -1);
			$due  = date('Y-m-d H:i:s', strtotime('+' . $durn . ' ' 
				  . str_replace( array(':i',':h',':d',':w',':m',':y'), 
			 				 	 array('minutes', 'hours', 'days', 'weeks', 'months', 'years'),
			 		 (':'.$durq)) 
				));
			return array( 'durn' => $durn, 'durq' => $durq, 'due' => $due);
		}
		return false;
	}
}