diff --git a/mod/acl.php b/mod/acl.php index e04a3fbc7e..e325e4668d 100644 --- a/mod/acl.php +++ b/mod/acl.php @@ -1,12 +1,309 @@ = `failure_update` + AND `notify` != '' $sql_extra2", + intval(local_user()) + ); + $contact_count = (int) $r[0]['c']; + } elseif ($type == 'f') { + // autocomplete for editor mentions of forums + $r = q("SELECT COUNT(*) AS c FROM `contact` + WHERE `uid` = %d AND NOT `self` + AND NOT `blocked` AND NOT `pending` AND NOT `archive` + AND (`forum` OR `prv`) + AND `success_update` >= `failure_update` + AND `notify` != '' $sql_extra2", + intval(local_user()) + ); + $contact_count = (int) $r[0]['c']; + } elseif ($type == 'm') { + // autocomplete for Private Messages + $r = q("SELECT COUNT(*) AS c FROM `contact` + WHERE `uid` = %d AND NOT `self` + AND NOT `blocked` AND NOT `pending` AND NOT `archive` + AND `success_update` >= `failure_update` + AND `network` IN ('%s', '%s') $sql_extra2", + intval(local_user()), + dbesc(NETWORK_DFRN), + dbesc(NETWORK_DIASPORA) + ); + $contact_count = (int) $r[0]['c']; + } elseif ($type == 'a') { + // autocomplete for Contacts + $r = q("SELECT COUNT(*) AS c FROM `contact` + WHERE `uid` = %d AND NOT `self` + AND NOT `pending` $sql_extra2", + intval(local_user()) + ); + $contact_count = (int) $r[0]['c']; + } else { + $contact_count = 0; + } + + $tot = $group_count + $contact_count; + + $groups = []; + $contacts = []; + + if ($type == '' || $type == 'g') { + /// @todo We should cache this query. + // This can be done when we can delete cache entries via wildcard + $r = q("SELECT `group`.`id`, `group`.`name`, GROUP_CONCAT(DISTINCT `group_member`.`contact-id` SEPARATOR ',') AS uids + FROM `group` + INNER JOIN `group_member` ON `group_member`.`gid`=`group`.`id` + WHERE NOT `group`.`deleted` AND `group`.`uid` = %d + $sql_extra + GROUP BY `group`.`name`, `group`.`id` + ORDER BY `group`.`name` + LIMIT %d,%d", + intval(local_user()), + intval($start), + intval($count) + ); + + foreach ($r as $g) { + $groups[] = [ + 'type' => 'g', + 'photo' => 'images/twopeople.png', + 'name' => htmlentities($g['name']), + 'id' => intval($g['id']), + 'uids' => array_map('intval', explode(',', $g['uids'])), + 'link' => '', + 'forum' => '0' + ]; + } + if ((count($groups) > 0) && ($search == '')) { + $groups[] = ['separator' => true]; + } + } + + if ($type == '') { + $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv`, (`prv` OR `forum`) AS `frm` FROM `contact` + WHERE `uid` = %d AND NOT `self` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != '' + AND `success_update` >= `failure_update` AND NOT (`network` IN ('%s', '%s')) + $sql_extra2 + ORDER BY `name` ASC ", + intval(local_user()), + dbesc(NETWORK_OSTATUS), + dbesc(NETWORK_STATUSNET) + ); + } elseif ($type == 'c') { + $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact` + WHERE `uid` = %d AND NOT `self` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != '' + AND `success_update` >= `failure_update` AND NOT (`network` IN ('%s')) + $sql_extra2 + ORDER BY `name` ASC ", + intval(local_user()), + dbesc(NETWORK_STATUSNET) + ); + } elseif ($type == 'f') { + $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact` + WHERE `uid` = %d AND NOT `self` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != '' + AND `success_update` >= `failure_update` AND NOT (`network` IN ('%s')) + AND (`forum` OR `prv`) + $sql_extra2 + ORDER BY `name` ASC ", + intval(local_user()), + dbesc(NETWORK_STATUSNET) + ); + } elseif ($type == 'm') { + $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr` FROM `contact` + WHERE `uid` = %d AND NOT `self` AND NOT `blocked` AND NOT `pending` AND NOT `archive` + AND `success_update` >= `failure_update` AND `network` IN ('%s', '%s') + $sql_extra2 + ORDER BY `name` ASC ", + intval(local_user()), + dbesc(NETWORK_DFRN), + dbesc(NETWORK_DIASPORA) + ); + } elseif ($type == 'a') { + $r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `addr`, `forum`, `prv` FROM `contact` + WHERE `uid` = %d AND `pending` = 0 AND `success_update` >= `failure_update` + $sql_extra2 + ORDER BY `name` ASC ", + intval(local_user()) + ); + } elseif ($type == 'x') { + // autocomplete for global contact search (e.g. navbar search) + $r = navbar_complete($a); + $contacts = []; + foreach ($r as $g) { + $contacts[] = [ + 'photo' => proxy_url($g['photo'], false, PROXY_SIZE_MICRO), + 'name' => $g['name'], + 'nick' => (x($g['addr']) ? $g['addr'] : $g['url']), + 'network' => $g['network'], + 'link' => $g['url'], + 'forum' => (x($g['community']) ? 1 : 0), + ]; + } + $o = [ + 'start' => $start, + 'count' => $count, + 'items' => $contacts, + ]; + echo json_encode($o); + killme(); + } else { + $r = []; + } + + if (DBM::is_result($r)) { + $forums = []; + foreach ($r as $g) { + $entry = [ + 'type' => 'c', + 'photo' => proxy_url($g['micro'], false, PROXY_SIZE_MICRO), + 'name' => htmlentities($g['name']), + 'id' => intval($g['id']), + 'network' => $g['network'], + 'link' => $g['url'], + 'nick' => htmlentities(($g['attag']) ? $g['attag'] : $g['nick']), + 'addr' => htmlentities(($g['addr']) ? $g['addr'] : $g['url']), + 'forum' => ((x($g, 'forum') || x($g, 'prv')) ? 1 : 0), + ]; + if ($entry['forum']) { + $forums[] = $entry; + } else { + $contacts[] = $entry; + } + } + if (count($forums) > 0) { + if ($search == '') { + $forums[] = ['separator' => true]; + } + $contacts = array_merge($forums, $contacts); + } + } + + $items = array_merge($groups, $contacts); + + if ($conv_id) { + /* + * if $conv_id is set, get unknown contacts in thread + * but first get known contacts url to filter them out + */ + $known_contacts = array_map(function ($i) { + return dbesc($i['link']); + }, $contacts); + + $unknown_contacts = []; + $r = q("SELECT `author-link` + FROM `item` WHERE `parent` = %d + AND (`author-name` LIKE '%%%s%%' OR `author-link` LIKE '%%%s%%') + AND `author-link` NOT IN ('%s') + GROUP BY `author-link`, `author-avatar`, `author-name` + ORDER BY `author-name` ASC + ", + intval($conv_id), + dbesc($search), + dbesc($search), + implode("', '", $known_contacts) + ); + if (DBM::is_result($r)) { + foreach ($r as $row) { + $contact = Contact::getDetailsByURL($row['author-link']); + + if (count($contact) > 0) { + $unknown_contacts[] = [ + 'type' => 'c', + 'photo' => proxy_url($contact['micro'], false, PROXY_SIZE_MICRO), + 'name' => htmlentities($contact['name']), + 'id' => intval($contact['cid']), + 'network' => $contact['network'], + 'link' => $contact['url'], + 'nick' => htmlentities($contact['nick'] ?: $contact['addr']), + 'addr' => htmlentities(($contact['addr']) ? $contact['addr'] : $contact['url']), + 'forum' => $contact['forum'] + ]; + } + } + } + + $items = array_merge($items, $unknown_contacts); + $tot += count($unknown_contacts); + } + + $results = [ + 'tot' => $tot, + 'start' => $start, + 'count' => $count, + 'groups' => $groups, + 'contacts' => $contacts, + 'items' => $items, + 'type' => $type, + 'search' => $search, + ]; + + Addon::callHooks('acl_lookup_end', $results); + + $o = [ + 'tot' => $results['tot'], + 'start' => $results['start'], + 'count' => $results['count'], + 'items' => $results['items'], + ]; + + echo json_encode($o); + + killme(); } - - diff --git a/src/Core/Acl.php b/src/Core/Acl.php new file mode 100644 index 0000000000..67e2633a2e --- /dev/null +++ b/src/Core/Acl.php @@ -0,0 +1,384 @@ + + */ +class Acl extends BaseObject +{ + /** + * Returns a select input tag with all the contact of the local user + * + * @param string $selname Name attribute of the select input tag + * @param string $selclass Class attribute of the select input tag + * @param array $options Available options: + * - size: length of the select box + * - mutual_friends: Only used for the hook + * - single: Only used for the hook + * - exclude: Only used for the hook + * @param array $preselected Contact ID that should be already selected + * @return string + */ + public static function getSuggestContactSelectHTML($selname, $selclass, array $options = [], array $preselected = []) + { + $a = self::getApp(); + + $networks = null; + + $size = defaults($options, 'size', 4); + $mutual = !empty($options['mutual_friends']); + $single = !empty($options['single']) && empty($options['multiple']); + $exclude = defaults($options, 'exclude', false); + + switch (defaults($options, 'networks', Protocol::PHANTOM)) { + case 'DFRN_ONLY': + $networks = [NETWORK_DFRN]; + break; + case 'PRIVATE': + if (!empty($a->user['prvnets'])) { + $networks = [NETWORK_DFRN, NETWORK_MAIL, NETWORK_DIASPORA]; + } else { + $networks = [NETWORK_DFRN, NETWORK_FACEBOOK, NETWORK_MAIL, NETWORK_DIASPORA]; + } + break; + case 'TWO_WAY': + if (!empty($a->user['prvnets'])) { + $networks = [NETWORK_DFRN, NETWORK_MAIL, NETWORK_DIASPORA]; + } else { + $networks = [NETWORK_DFRN, NETWORK_FACEBOOK, NETWORK_MAIL, NETWORK_DIASPORA, NETWORK_OSTATUS]; + } + break; + default: /// @TODO Maybe log this call? + break; + } + + $x = ['options' => $options, 'size' => $size, 'single' => $single, 'mutual' => $mutual, 'exclude' => $exclude, 'networks' => $networks]; + + Addon::callHooks('contact_select_options', $x); + + $o = ''; + + $sql_extra = ''; + + if (!empty($x['mutual'])) { + $sql_extra .= sprintf(" AND `rel` = %d ", intval(CONTACT_IS_FRIEND)); + } + + if (!empty($x['exclude'])) { + $sql_extra .= sprintf(" AND `id` != %d ", intval($x['exclude'])); + } + + if (!empty($x['networks'])) { + /// @TODO rewrite to foreach() + array_walk($x['networks'], function (&$value) { + $value = "'" . dbesc($value) . "'"; + }); + $str_nets = implode(',', $x['networks']); + $sql_extra .= " AND `network` IN ( $str_nets ) "; + } + + $tabindex = (!empty($options['tabindex']) ? 'tabindex="' . $options["tabindex"] . '"' : ''); + + if (!empty($x['single'])) { + $o .= "\r\n"; + } + + $stmt = dba::p("SELECT `id`, `name`, `url`, `network` FROM `contact` + WHERE `uid` = %d AND NOT `self` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != '' + $sql_extra + ORDER BY `name` ASC ", intval(local_user()) + ); + + $contacts = dba::inArray($stmt); + + $arr = ['contact' => $contacts, 'entry' => $o]; + + // e.g. 'network_pre_contact_deny', 'profile_pre_contact_allow' + Addon::callHooks($a->module . '_pre_' . $selname, $arr); + + if (DBM::is_result($contacts)) { + foreach ($contacts as $contact) { + if (in_array($contact['id'], $preselected)) { + $selected = ' selected="selected" '; + } else { + $selected = ''; + } + + $trimmed = mb_substr($contact['name'], 0, 20); + + $o .= "\r\n"; + } + } + + $o .= '' . PHP_EOL; + + Addon::callHooks($a->module . '_post_' . $selname, $o); + + return $o; + } + + /** + * Returns a select input tag with all the contact of the local user + * + * @param string $selname Name attribute of the select input tag + * @param string $selclass Class attribute of the select input tag + * @param array $preselected Contact ID that should be already selected + * @param int $size Length of the select box + * @param bool $privmail + * @param bool $celeb + * @param bool $privatenet + * @param int $tabindex Select input tag tabindex attribute + * @return string + */ + public static function getMessageContactSelectHTML( + $selname, $selclass, array $preselected = [], $size = 4, $privmail = false, $celeb = false, $privatenet = false, + $tabindex = null) + { + $a = self::getApp(); + + $o = ''; + + // When used for private messages, we limit correspondence to mutual DFRN/Friendica friends and the selector + // to one recipient. By default our selector allows multiple selects amongst all contacts. + + $sql_extra = ''; + + if ($privmail || $celeb) { + $sql_extra .= sprintf(" AND `rel` = %d ", intval(CONTACT_IS_FRIEND)); + } + + if ($privmail) { + $sql_extra .= sprintf(" AND `network` IN ('%s' , '%s') ", NETWORK_DFRN, NETWORK_DIASPORA); + } elseif ($privatenet) { + $sql_extra .= sprintf(" AND `network` IN ('%s' , '%s', '%s', '%s') ", NETWORK_DFRN, NETWORK_MAIL, NETWORK_FACEBOOK, + NETWORK_DIASPORA); + } + + $tabindex_attr = !empty($tabindex) ? ' tabindex="' . intval($tabindex) . '"' : ''; + + if ($privmail && $preselected) { + $sql_extra .= " AND `id` IN (" . implode(",", $preselected) . ")"; + $hidepreselected = ' style="display: none;"'; + } else { + $hidepreselected = ''; + } + + if ($privmail) { + $o .= "\r\n"; + } + + $stmt = dba::p("SELECT `id`, `name`, `url`, `network` FROM `contact` + WHERE `uid` = %d AND NOT `self` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != '' + $sql_extra + ORDER BY `name` ASC ", intval(local_user()) + ); + + $contacts = dba::inArray($stmt); + + $arr = ['contact' => $contacts, 'entry' => $o]; + + // e.g. 'network_pre_contact_deny', 'profile_pre_contact_allow' + + Addon::callHooks($a->module . '_pre_' . $selname, $arr); + + $receiverlist = []; + + if (DBM::is_result($contacts)) { + foreach ($contacts as $contact) { + if (in_array($contact['id'], $preselected)) { + $selected = ' selected="selected"'; + } else { + $selected = ''; + } + + if ($privmail) { + $trimmed = Protocol::formatMention($contact['url'], $contact['name']); + } else { + $trimmed = mb_substr($contact['name'], 0, 20); + } + + $receiverlist[] = $trimmed; + + $o .= "\r\n"; + } + } + + $o .= '' . PHP_EOL; + + if ($privmail && $preselected) { + $o .= implode(', ', $receiverlist); + } + + Addon::callHooks($a->module . '_post_' . $selname, $o); + + return $o; + } + + /** + * Return the default permission of the provided user array + * + * @param array $user + * @return array Hash of contact id lists + */ + public static function getDefaultUserPermissions(array $user = null) + { + $matches = []; + + $acl_regex = '/<([0-9]+)>/i'; + + preg_match_all($acl_regex, defaults($user, 'allow_cid', ''), $matches); + $allow_cid = $matches[1]; + preg_match_all($acl_regex, defaults($user, 'allow_gid', ''), $matches); + $allow_gid = $matches[1]; + preg_match_all($acl_regex, defaults($user, 'deny_cid', ''), $matches); + $deny_cid = $matches[1]; + preg_match_all($acl_regex, defaults($user, 'deny_gid', ''), $matches); + $deny_gid = $matches[1]; + + Contact::pruneUnavailable($allow_cid); + + return [ + 'allow_cid' => $allow_cid, + 'allow_gid' => $allow_gid, + 'deny_cid' => $deny_cid, + 'deny_gid' => $deny_gid, + ]; + } + + /** + * Return the full jot ACL selector HTML + * + * @param array $user + * @param bool $show_jotnets + * @return string + */ + public static function getFullSelectorHTML(array $user = null, $show_jotnets = false) + { + $perms = self::getDefaultUserPermissions($user); + + $jotnets = ''; + if ($show_jotnets) { + $imap_disabled = !function_exists('imap_open') || Config::get('system', 'imap_disabled'); + + $mail_enabled = false; + $pubmail_enabled = false; + + if (!$imap_disabled) { + $mailacct = dba::selectFirst('mailacct', ['pubmail'], ['`uid` = ? AND `server` != ""', local_user()]); + if (DBM::is_result($mailacct)) { + $mail_enabled = true; + $pubmail_enabled = !empty($mailacct['pubmail']); + } + } + + if (empty($user['hidewall'])) { + if ($mail_enabled) { + $selected = $pubmail_enabled ? ' checked="checked"' : ''; + $jotnets .= '
' . L10n::t("Post to Email") . '
'; + } + + Addon::callHooks('jot_networks', $jotnets); + } else { + $jotnets .= L10n::t('Connectors disabled, since "%s" is enabled.', + L10n::t('Hide your profile details from unknown viewers?')); + } + } + + $tpl = get_markup_template('acl_selector.tpl'); + $o = replace_macros($tpl, [ + '$showall' => L10n::t('Visible to everybody'), + '$show' => L10n::t('show'), + '$hide' => L10n::t('don\'t show'), + '$allowcid' => json_encode($perms['allow_cid']), + '$allowgid' => json_encode($perms['allow_gid']), + '$denycid' => json_encode($perms['deny_cid']), + '$denygid' => json_encode($perms['deny_gid']), + '$networks' => $show_jotnets, + '$emailcc' => L10n::t('CC: email addresses'), + '$emtitle' => L10n::t('Example: bob@example.com, mary@example.com'), + '$jotnets' => $jotnets, + '$aclModalTitle' => L10n::t('Permissions'), + '$aclModalDismiss' => L10n::t('Close'), + '$features' => [ + 'aclautomention' => Feature::isEnabled($user['uid'], 'aclautomention') ? 'true' : 'false' + ], + ]); + + return $o; + } + + /** + * Searching for global contacts for autocompletion + * + * @brief Searching for global contacts for autocompletion + * @param string $search Name or part of a name or nick + * @param string $mode Search mode (e.g. "community") + * @return array with the search results + */ + public static function contactAutocomplete($search, $mode) + { + if ((Config::get('system', 'block_public')) && (!local_user()) && (!remote_user())) { + return []; + } + + // don't search if search term has less than 2 characters + if (!$search || mb_strlen($search) < 2) { + return []; + } + + if (substr($search, 0, 1) === '@') { + $search = substr($search, 1); + } + + // check if searching in the local global contact table is enabled + if (Config::get('system', 'poco_local_search')) { + $return = GContact::searchByName($search, $mode); + } else { + $a = self::getApp(); + $p = $a->pager['page'] != 1 ? '&p=' . $a->pager['page'] : ''; + + $response = Network::curl(get_server() . '/lsearch?f=' . $p . '&search=' . urlencode($search)); + if ($response['success']) { + $lsearch = json_decode($response['body'], true); + if (!empty($lsearch['results'])) { + $return = $lsearch['results']; + } + } + } + + return defaults($return, []); + } +} diff --git a/src/Model/Contact.php b/src/Model/Contact.php index fc27b0c410..85e71075a9 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -1517,4 +1517,27 @@ class Contact extends BaseObject } } } + + /** + * Remove the unavailable contact ids from the provided list + * + * @param array $contact_ids Contact id list + */ + public static function pruneUnavailable(array &$contact_ids) + { + if (empty($contact_ids)) { + return; + } + + $str = dbesc(implode(',', $contact_ids)); + + $stmt = dba::p("SELECT `id` FROM `contact` WHERE `id` IN ( " . $str . ") AND `blocked` = 0 AND `pending` = 0 AND `archive` = 0"); + + $return = []; + while($contact = dba::fetch($stmt)) { + $return[] = $contact['id']; + } + + $contact_ids = $return; + } }