<?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); } $langs = array_keys($tpla); asort($langs); $tplx = array_unique($tplx); asort($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; } }