diff options
Diffstat (limited to 'Zotlabs/Module/Invite.php')
-rw-r--r-- | Zotlabs/Module/Invite.php | 599 |
1 files changed, 497 insertions, 102 deletions
diff --git a/Zotlabs/Module/Invite.php b/Zotlabs/Module/Invite.php index 6359da54c..24792c8c1 100644 --- a/Zotlabs/Module/Invite.php +++ b/Zotlabs/Module/Invite.php @@ -6,7 +6,7 @@ use Zotlabs\Lib\Apps; use Zotlabs\Web\Controller; /** - * module: invite.php + * module: invitexv2.php * * send email invitations to join social network * @@ -15,91 +15,291 @@ use Zotlabs\Web\Controller; 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( t('Permission denied.') . EOL); + if (! local_channel()) { + notice( 'ZAI0201E,' .t('Permission denied.') . EOL); return; } - if(! Apps::system_app_installed(local_channel(), 'Invite')) { + 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'); - - $max_invites = intval(get_config('system','max_invites')); - if(! $max_invites) - $max_invites = 50; - - $current_invites = intval(get_pconfig(local_channel(),'system','sent_invites')); - if($current_invites > $max_invites) { - notice( t('Total invitation limit exceeded.') . EOL); + + $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; - }; - - - $recips = ((x($_POST,'recipients')) ? explode("\n",$_POST['recipients']) : array()); - $message = ((x($_POST,'message')) ? notags(trim($_POST['message'])) : ''); - - $total = 0; - - if(get_config('system','invitation_only')) { - $invonly = true; - $x = get_pconfig(local_channel(),'system','invites_remaining'); - if((! $x) && (! is_site_admin())) - 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; - foreach($recips as $recip) { - - $recip = trim($recip); - if(! $recip) - continue; - - if(! validate_email($recip)) { - notice( sprintf( t('%s : Not a valid email address.'), $recip) . EOL); - continue; + // 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) { - else - $nmessage = $message; - - $account = App::get_account(); + $reonar['due'] = $due; + $reonar['to'] = $recip; + $reonar['txtpersonal'] = $mailtext; - $res = z_mail( + // 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' => $account['account_email'], - 'messageSubject' => t('Please join us on $Projectname'), - 'textVersion' => $nmessage, + 'fromEmail' => $reonar['from'], + 'messageSubject' => $reonar['subject'], + 'textVersion' => ($mailtext ? $mailtext . "\n\n" : '') . $tx . "\n" . $due, ] ); - if($res) { - $total ++; - $current_invites ++; - set_pconfig(local_channel(),'system','sent_invites',$current_invites); - if($current_invites > $max_invites) { - notice( t('Invitation limit exceeded. Please contact your site administrator.') . EOL); - return; - } - } - else { - notice( sprintf( t('%s : Message delivery failed.'), $recip) . EOL); + 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); } - notice( sprintf( tt("%d message sent.", "%d messages sent.", $total) , $total) . EOL); + + $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( t('Permission denied.') . EOL); + notice( 'ZAI0101E,' . t('Permission denied.') . EOL); return; } @@ -107,68 +307,263 @@ class Invite extends Controller { //Do not display any associated widgets at this point App::$pdl = ''; - $o = '<b>' . t('Invite App') . ' (' . t('Not Installed') . '):</b><br>'; - $o .= t('Send email invitations to join this network'); + $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'); - $invonly = false; + + $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'; - if(get_config('system','invitation_only')) { - $invonly = true; - $x = get_pconfig(local_channel(),'system','invites_remaining'); - if((! $x) && (! is_site_admin())) { - notice( t('You have no more invitations available') . EOL); - return ''; + // 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); } - - if($invonly && ($x || is_site_admin())) { - $invite_code = autoname(8) . rand(1000,9999); - $nmessage = str_replace('$invite_code',$invite_code,$message); - - $r = q("INSERT INTO register (hash,created,uid,password,lang) VALUES ('%s', '%s',0,'','') ", - dbesc($invite_code), - dbesc(datetime_convert()) + 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' + ) ); - - if(! is_site_admin()) { - $x --; - if($x >= 0) - set_pconfig(local_channel(),'system','invites_remaining',$x); - else - return; - } + + // 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"; } - - $ob = App::get_observer(); - if(! $ob) - return $o; - - $channel = App::get_channel(); - + } + + // 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'), - '$addr_text' => t('Enter email addresses, one per line:'), - '$msg_text' => t('Your message:'), - '$default_message' => t('Please join my community on $Projectname.') . "\r\n" . "\r\n" - . $linktxt - . (($invonly) ? "\r\n" . "\r\n" . t('You will need to supply this invitation code:') . " " . $invite_code . "\r\n" . "\r\n" : '') - . t('1. Register at any $Projectname location (they are all inter-connected)') - . "\r\n" . "\r\n" . z_root() . '/register' - . "\r\n" . "\r\n" . t('2. Enter my $Projectname network address into the site searchbar.') - . "\r\n" . "\r\n" . $ob['xchan_addr'] . ' (' . t('or visit') . " " . z_root() . '/channel/' . $channel['channel_address'] . ')' - . "\r\n" . "\r\n" - . t('3. Click [Connect]') - . "\r\n" . "\r\n" , + '$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; + } } + |