diff options
Diffstat (limited to 'include')
36 files changed, 2070 insertions, 1599 deletions
diff --git a/include/account.php b/include/account.php index 884c07389..0c07bd85f 100644 --- a/include/account.php +++ b/include/account.php @@ -17,10 +17,38 @@ require_once('include/crypto.php'); require_once('include/channel.php'); -function get_account_by_id($account_id) { - $r = q("select * from account where account_id = %d", - intval($account_id) - ); +/** + * Returns the id of a locally logged in account or false. + * + * Returns the numeric account id of the current session if authenticated, or + * false otherwise. + * + * @note It is possible to be authenticated, and not connected to a channel. + * + * @return int|false Numeric account id or false. + */ +function get_account_id(): int|false { + if (isset($_SESSION['account_id'])) { + return intval($_SESSION['account_id']); + } + + if (App::$account) { + return intval(App::$account['account_id']); + } + + return false; +} + +/** + * Get the account with the given id from the database. + * + * @param int $account_id The numeric id of the account to fetch. + * + * @return array|false An array containing the attributes of the requested + * account, or false if it could not be retreived. + */ +function get_account_by_id(int $account_id): array|false { + $r = q("select * from account where account_id = %d", $account_id); return (($r) ? $r[0] : false); } @@ -117,11 +145,16 @@ function check_account_invite($invite_code) { } function check_account_admin($arr) { - if(is_site_admin()) + if (is_site_admin()) { return true; + } + $admin_email = trim(Config::Get('system','admin_email')); - if(strlen($admin_email) && $admin_email === trim($arr['email'])) + + if (strlen($admin_email) && $admin_email === trim($arr['reg_email'])) { return true; + } + return false; } @@ -132,167 +165,6 @@ function account_total() { return false; } -// legacy -function account_store_lowlevel_IS_OBSOLETE($arr) { - - $store = [ - 'account_parent' => ((array_key_exists('account_parent',$arr)) ? $arr['account_parent'] : '0'), - 'account_default_channel' => ((array_key_exists('account_default_channel',$arr)) ? $arr['account_default_channel'] : '0'), - 'account_salt' => ((array_key_exists('account_salt',$arr)) ? $arr['account_salt'] : ''), - 'account_password' => ((array_key_exists('account_password',$arr)) ? $arr['account_password'] : ''), - 'account_email' => ((array_key_exists('account_email',$arr)) ? $arr['account_email'] : ''), - 'account_external' => ((array_key_exists('account_external',$arr)) ? $arr['account_external'] : ''), - 'account_language' => ((array_key_exists('account_language',$arr)) ? $arr['account_language'] : 'en'), - 'account_created' => ((array_key_exists('account_created',$arr)) ? $arr['account_created'] : '0001-01-01 00:00:00'), - 'account_lastlog' => ((array_key_exists('account_lastlog',$arr)) ? $arr['account_lastlog'] : '0001-01-01 00:00:00'), - 'account_flags' => ((array_key_exists('account_flags',$arr)) ? $arr['account_flags'] : '0'), - 'account_roles' => ((array_key_exists('account_roles',$arr)) ? $arr['account_roles'] : '0'), - 'account_reset' => ((array_key_exists('account_reset',$arr)) ? $arr['account_reset'] : ''), - 'account_expires' => ((array_key_exists('account_expires',$arr)) ? $arr['account_expires'] : '0001-01-01 00:00:00'), - 'account_expire_notified' => ((array_key_exists('account_expire_notified',$arr)) ? $arr['account_expire_notified'] : '0001-01-01 00:00:00'), - 'account_service_class' => ((array_key_exists('account_service_class',$arr)) ? $arr['account_service_class'] : ''), - 'account_level' => '5', - 'account_password_changed' => ((array_key_exists('account_password_changed',$arr)) ? $arr['account_password_changed'] : '0001-01-01 00:00:00') - ]; - - // never ever is this a create table but a pdo insert into account - // strange function placement in text.php (obscure by design :-) - return create_table_from_array('account',$store); - // the TODO may be to adjust others using create_table_from_array(): - // channel.php - // connections.php - // event.php - // hubloc.php - // import.php -} - - - -// legacy -function create_account_IS_OBSOLETE($arr) { - - // Required: { email, password } - - $result = array('success' => false, 'email' => '', 'password' => '', 'message' => ''); - - $invite_code = ((x($arr,'invite_code')) ? notags(trim($arr['invite_code'])) : ''); - $email = ((x($arr,'email')) ? notags(punify(trim($arr['email']))) : ''); - $password = ((x($arr,'password')) ? trim($arr['password']) : ''); - $parent = ((x($arr,'parent')) ? intval($arr['parent']) : 0 ); - $flags = ((x($arr,'account_flags')) ? intval($arr['account_flags']) : ACCOUNT_OK); - $roles = ((x($arr,'account_roles')) ? intval($arr['account_roles']) : 0 ); - $expires = ((x($arr,'expires')) ? intval($arr['expires']) : NULL_DATE); - - $default_service_class = Config::Get('system','default_service_class'); - - if($default_service_class === false) - $default_service_class = ''; - - if((! x($email)) || (! x($password))) { - $result['message'] = t('Please enter the required information.'); - return $result; - } - - // prevent form hackery - - if($roles & ACCOUNT_ROLE_ADMIN) { - $admin_result = check_account_admin($arr); - if(! $admin_result) { - $roles = 0; - } - } - - // allow the admin_email account to be admin, but only if it's the first account. - - $c = account_total(); - if (($c === 0) && (check_account_admin($arr))) - $roles |= ACCOUNT_ROLE_ADMIN; - - // Ensure that there is a host keypair. - - if ((! Config::Get('system', 'pubkey')) && (! Config::Get('system', 'prvkey'))) { - $hostkey = Crypto::new_keypair(4096); - Config::Set('system', 'pubkey', $hostkey['pubkey']); - Config::Set('system', 'prvkey', $hostkey['prvkey']); - } - - $invite_result = check_account_invite($invite_code); - if($invite_result['error']) { - $result['message'] = $invite_result['message']; - return $result; - } - - $email_result = check_account_email($email); - - if($email_result['error']) { - $result['message'] = $email_result['message']; - return $result; - } - - $password_result = check_account_password($password); - - if($password_result['error']) { - $result['message'] = $password_result['message']; - return $result; - } - - $salt = random_string(32); - $password_encoded = hash('whirlpool', $salt . $password); - - $r = account_store_lowlevel( - [ - 'account_parent' => intval($parent), - 'account_salt' => $salt, - 'account_password' => $password_encoded, - 'account_email' => $email, - 'account_language' => get_best_language(), - 'account_created' => datetime_convert(), - 'account_flags' => intval($flags), - 'account_roles' => intval($roles), - 'account_level' => 5, - 'account_expires' => $expires, - 'account_service_class' => $default_service_class - ] - ); - if(! $r) { - logger('create_account: DB INSERT failed.'); - $result['message'] = t('Failed to store account information.'); - return($result); - } - - $r = q("select * from account where account_email = '%s' and account_password = '%s' limit 1", - dbesc($email), - dbesc($password_encoded) - ); - if($r && count($r)) { - $result['account'] = $r[0]; - } - else { - logger('create_account: could not retrieve newly created account'); - } - - // Set the parent record to the current record_id if no parent was provided - - if(! $parent) { - $r = q("update account set account_parent = %d where account_id = %d", - intval($result['account']['account_id']), - intval($result['account']['account_id']) - ); - if(! $r) { - logger('create_account: failed to set parent'); - } - $result['account']['parent'] = $result['account']['account_id']; - } - - $result['success'] = true; - $result['email'] = $email; - $result['password'] = $password; - - call_hooks('register_account',$result); - - return $result; -} - /** * create_account_from_register * @author hilmar runge @@ -324,18 +196,18 @@ function create_account_from_register($arr) { if($default_service_class === false) $default_service_class = ''; - $roles = 0; - // prevent form hackery - if($roles & ACCOUNT_ROLE_ADMIN) { - $admin_result = check_account_admin($arr); - if(! $admin_result) { - $roles = 0; - } + // any accounts available ? + $total = q("SELECT COUNT(*) AS total FROM account"); + + if ($total && intval($total[0]['total']) === 0 && !check_account_admin($register[0])) { + logger('create_account: first account is not admin'); + $result['message'] = t('First account is not admin.'); + return $result; } - // any accounts available ? - $isa = q("SELECT COUNT(*) AS isa FROM account"); - if ($isa && $isa[0]['isa'] == 0) { + $roles = 0; + + if (check_account_admin($register[0])) { $roles = ACCOUNT_ROLE_ADMIN; } @@ -446,76 +318,6 @@ function verify_email_address($arr) { return $res; } -function verify_email_addressNOP($arr) { - - if(array_key_exists('resend',$arr)) { - $a = q("select * from account where account_email = '%s' limit 1", - dbesc($arr['email']) - ); - if(! ($a && ($a[0]['account_flags'] & ACCOUNT_UNVERIFIED))) { - return false; - } - $account = $a[0]; - // [hilmar -> - $v = q("SELECT * FROM register WHERE reg_uid = %d AND reg_vital = 1 " - . " AND reg_pass = 'verify' LIMIT 1", - intval($account['account_id']) - ); - // <- hilmar] - if($v) { - $hash = $v[0]['reg_hash']; - } - else { - return false; - } - } - else { - $hash = random_string(24); - - // [hilmar -> - q("INSERT INTO register ( reg_hash, reg_created, reg_uid, reg_pass, reg_lang, reg_stuff ) " - ." VALUES ( '%s', '%s', %d, '%s', '%s', '' ) ", - dbesc($hash), - dbesc(datetime_convert()), - intval($arr['account']['account_id']), - dbesc('verify'), - dbesc($arr['account']['account_language']) - ); - // <- hilmar] - $account = $arr['account']; - } - - push_lang(($account['account_language']) ? $account['account_language'] : 'en'); - - $email_msg = replace_macros(get_intltext_template('register_verify_member.tpl'), - [ - '$sitename' => Config::Get('system','sitename'), - '$siteurl' => z_root(), - '$email' => $arr['email'], - '$uid' => $account['account_id'], - '$hash' => $hash, - '$details' => '' - ] - ); - - $res = z_mail( - [ - 'toEmail' => $arr['email'], - 'messageSubject' => sprintf( t('Registration confirmation for %s'), Config::Get('system','sitename')), - 'textVersion' => $email_msg, - ] - ); - - pop_lang(); - - if(! $res) - logger('send_reg_approval_email: failed to account_id: ' . $arr['account']['account_id']); - - return $res; -} - - - function send_reg_approval_email($arr) { @@ -613,59 +415,45 @@ function send_register_success_email($email,$password) { } /** - * @brief Allows a user registration. + * Mark a pending registration as approved, and notify the account + * holder by email. * - * @param string $hash - * @return array|boolean + * @param string $hash The registration hash of the entry to approve + * + * @return bool */ -function account_allow($hash) { - - $ret = array('success' => false); +function account_allow(string $hash): bool { $register = q("SELECT * FROM register WHERE reg_hash = '%s' LIMIT 1", dbesc($hash) ); - if(! $register) - return $ret; + if (! $register) { + logger( + "Entry with hash '{$hash}' was not found in the register table.", + LOGGER_NORMAL, + LOG_ERR + ); + return false; + } - $account = q("SELECT * FROM account WHERE account_id = %d LIMIT 1", - intval($register[0]['reg_uid']) - ); + $account = get_account_by_id($register[0]['reg_uid']); - // a register entry without account assigned to - if(! $account) - return $ret; + if (! $account) { + logger( + "Account '{$register[0]['reg_uid']}' mentioned by registration hash '{$hash}' was not found.", + LOGGER_NORMAL, + LOG_ERR + ); + return false; + } - // [hilmar -> + $transaction = new DbaTransaction(DBA::$dba); - q("START TRANSACTION"); - //q("DELETE FROM register WHERE reg_hash = '%s'", - // dbesc($register[0]['reg_hash']) - //); $r1 = q("UPDATE register SET reg_vital = 0 WHERE reg_hash = '%s'", dbesc($register[0]['reg_hash']) ); - /* instead of ... - - // unblock - q("UPDATE account SET account_flags = (account_flags & ~%d) " - . " WHERE (account_flags & %d)>0 AND account_id = %d", - intval(ACCOUNT_BLOCKED), - intval(ACCOUNT_BLOCKED), - intval($register[0]['reg_uid']) - ); - - // unpend - q("UPDATE account SET account_flags = (account_flags & ~%d) " - . " WHERE (account_flags & %d)>0 AND account_id = %d", - intval(ACCOUNT_PENDING), - intval(ACCOUNT_PENDING), - intval($register[0]['reg_uid']) - ); - - */ // together unblock and unpend $r2 = q("UPDATE account SET account_flags = %d WHERE account_id = %d", intval($account['account_flags'] @@ -674,9 +462,7 @@ function account_allow($hash) { ); if($r1 && $r2) { - q("COMMIT"); - - // <- hilmar] + $transaction->commit(); push_lang($register[0]['reg_lang']); @@ -684,35 +470,35 @@ function account_allow($hash) { $email_msg = replace_macros($email_tpl, array( '$sitename' => Config::Get('system','sitename'), '$siteurl' => z_root(), - '$username' => $account[0]['account_email'], - '$email' => $account[0]['account_email'], + '$username' => $account['account_email'], + '$email' => $account['account_email'], '$password' => '', - '$uid' => $account[0]['account_id'] + '$uid' => $account['account_id'] )); $res = z_mail( [ - 'toEmail' => $account[0]['account_email'], + 'toEmail' => $account['account_email'], 'messageSubject' => sprintf( t('Registration details for %s'), Config::Get('system','sitename')), 'textVersion' => $email_msg, ] ); - pop_lang(); + if (! $res) { + info(t("Sending account approval email to {$account['email']} failed...")); + } - if(Config::Get('system', 'auto_channel_create', 1)) - auto_channel_create($register[0]['uid']); + pop_lang(); - if ($res) { - info( t('Account approved.') . EOL ); - return true; + if(Config::Get('system', 'auto_channel_create', 1)) { + auto_channel_create($register[0]['reg_uid']); } - // [hilmar -> - } else { - q("ROLLBACK"); + info( t('Account approved.') . EOL ); + return true; } - // <- hilmar] + + return false; } diff --git a/include/acl_selectors.php b/include/acl_selectors.php index f158a439b..f0e0140dc 100644 --- a/include/acl_selectors.php +++ b/include/acl_selectors.php @@ -26,7 +26,7 @@ function populate_acl($defaults = null,$show_jotnets = true, $emptyACL_descripti $allow_cid = $allow_gid = $deny_cid = $deny_gid = false; $showall_origin = ''; - $showall_icon = 'fa-globe'; + $showall_icon = 'bi-globe'; $role = get_pconfig(local_channel(), 'system', 'permissions_role'); if(! $emptyACL_description) { diff --git a/include/api_zot.php b/include/api_zot.php index 56cec005d..a2753269d 100644 --- a/include/api_zot.php +++ b/include/api_zot.php @@ -546,15 +546,13 @@ return false; } - - logger('api_red_item_store: REQUEST ' . print_r($_REQUEST,true)); + logger('api_red_item_store: REQUEST ' . print_r($_POST,true)); logger('api_red_item_store: FILES ' . print_r($_FILES,true)); - // set this so that the item_post() function is quiet and doesn't redirect or emit json - $_REQUEST['api_source'] = true; - $_REQUEST['profile_uid'] = api_user(); + $_POST['api_source'] = true; + $_POST['profile_uid'] = api_user(); if(x($_FILES,'media')) { $_FILES['userfile'] = $_FILES['media']; @@ -562,11 +560,12 @@ $mod = new Zotlabs\Module\Wall_attach(); $media = $mod->post(); if($media) - $_REQUEST['body'] = $media . "\n" . $_REQUEST['body']; + $_POST['body'] = $media . "\n" . $_POST['body']; } $mod = new Zotlabs\Module\Item(); $x = $mod->post(); + json_return_and_die($x); } diff --git a/include/attach.php b/include/attach.php index 449721793..0569b97fb 100644 --- a/include/attach.php +++ b/include/attach.php @@ -63,6 +63,7 @@ function z_mime_content_type($filename) { 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'webp' => 'image/webp', + 'avif' => 'image/avif', 'bmp' => 'image/bmp', 'ico' => 'image/vnd.microsoft.icon', 'tiff' => 'image/tiff', @@ -673,7 +674,7 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) { logger('getimagesize: ' . print_r($gis,true), LOGGER_DATA); } - if(($gis) && ($gis[2] === IMAGETYPE_GIF || $gis[2] === IMAGETYPE_JPEG || $gis[2] === IMAGETYPE_PNG || $gis[2] === IMAGETYPE_WEBP)) { + if(($gis) && ($gis[2] === IMAGETYPE_GIF || $gis[2] === IMAGETYPE_JPEG || $gis[2] === IMAGETYPE_PNG || $gis[2] === IMAGETYPE_WEBP || $gis[2] === IMAGETYPE_AVIF)) { $is_photo = 1; if($gis[2] === IMAGETYPE_GIF) $def_extension = '.gif'; @@ -683,6 +684,8 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) { $def_extension = '.png'; if($gis[2] === IMAGETYPE_WEBP) $def_extension = '.webp'; + if($gis[2] === IMAGETYPE_AVIF) + $def_extension = '.avif'; } // If we know it's a photo, over-ride the type in case the source system could not determine what it was @@ -796,6 +799,12 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) { } } + if (mb_strlen($filename, 'UTF-8') > MAX_FILENAME_LENGTH) { + logger('filename too long'); + $ret['message'] = t('Filename too long'); + return $ret; + } + if(! $hash) $hash = new_uuid(); @@ -1180,11 +1189,17 @@ function attach_mkdir($channel, $observer_hash, $arr = null) { return $ret; } - if(isset($arr['filename']) && !strlen($arr['filename'])) { + if(empty($arr['filename'])) { $ret['message'] = t('Empty pathname'); return $ret; } + if(mb_strlen($arr['filename'], 'UTF-8') > MAX_FOLDER_LENGTH) { + logger('pathname too long'); + $ret['message'] = t('Pathname too long'); + return $ret; + } + $arr['hash'] = $arr['hash'] ?? new_uuid(); // Check for duplicate name. @@ -1606,8 +1621,7 @@ function attach_drop_photo($channel_id,$resource) { if($x) { $stage = (($x[0]['item_hidden']) ? DROPITEM_NORMAL : DROPITEM_PHASE1); - $interactive = (($x[0]['item_hidden']) ? false : true); - drop_item($x[0]['id'], $interactive, $stage); + drop_item($x[0]['id'], $stage); } $r = q("SELECT content FROM photo WHERE resource_id = '%s' AND uid = %d AND os_storage = 1", @@ -1636,8 +1650,7 @@ function attach_drop_item($channel_id,$resource) { if($x) { $stage = (($x[0]['item_hidden']) ? DROPITEM_NORMAL : DROPITEM_PHASE1); - $interactive = (($x[0]['item_hidden']) ? false : true); - drop_item($x[0]['id'], $interactive, $stage); + drop_item($x[0]['id'], $stage); } } @@ -1855,8 +1868,6 @@ function pipe_streams($in, $out, $bufsize = 16384) { } function attach_store_item($channel, $observer, $file) { - - if(is_string($file)) { $r = q("SELECT * FROM attach WHERE uid = %d AND hash = '%s' LIMIT 1", intval($channel['channel_id']), @@ -1906,10 +1917,11 @@ function attach_store_item($channel, $observer, $file) { $post = item_store($arr); - $item_id = $post['item_id']; - - if($item_id) { - Master::Summon(['Notifier', 'activity', $item_id]); + if ($post['success']) { + Master::Summon(['Notifier', 'activity', $post['item_id']]); + if (!empty($post['approval_id'])) { + Master::Summon(['Notifier', 'activity', $post['approval_id']]); + } } */ @@ -1919,8 +1931,11 @@ function attach_store_item($channel, $observer, $file) { } $stage = (($r[0]['item_hidden']) ? DROPITEM_NORMAL : DROPITEM_PHASE1); - $interactive = (($r[0]['item_hidden']) ? false : true); - drop_item($r[0]['id'], $interactive, $stage); + drop_item($r[0]['id'], $stage); + + if (empty($r[0]['item_hidden'])) { + Master::Summon(['Notifier', 'drop', $r[0]['id']]); + } } @@ -1945,6 +1960,12 @@ function attach_store_item($channel, $observer, $file) { $mid = z_root() . '/item/' . $uuid; + $target = [ + 'id' => z_root() . '/conversation/' . $uuid, + 'type' => 'Collection', + 'attributedTo' => channel_url($channel), + ]; + $arr = []; // Initialize the array of parameters for the post $arr['aid'] = $channel['channel_account_id']; $arr['uuid'] = $uuid; @@ -1965,6 +1986,8 @@ function attach_store_item($channel, $observer, $file) { $arr['item_thread_top'] = 1; $arr['item_private'] = (($file['allow_cid'] || $file['allow_gid'] || $file['deny_cid'] || $file['deny_gid']) ? 1 : 0); $arr['verb'] = 'Create'; + $arr['target'] = $target; + $arr['target_type'] = 'Collection'; $arr['obj_type'] = $type; $arr['title'] = $file['filename']; @@ -1982,14 +2005,13 @@ function attach_store_item($channel, $observer, $file) { } $body_str = sprintf((($type === 'Image') ? t('%s shared an %s with you') : t('%s shared a %s with you')), '[zrl=' . $observer['xchan_url'] . ']' . $observer['xchan_name'] . '[/zrl]', '[zrl=' . $path . ']' . (($type === 'Image') ? t('image') : t('file')) . '[/zrl]'); - $arr['body'] .= $body_str; + $arr['body'] .= "\r\n" . $body_str; $meta = [ 'name' => $file['filename'], 'type' => $file['filetype'], 'size' => $file['filesize'], 'revision' => $file['revision'], - 'size' => $file['filesize'], 'created' => $file['created'], 'edited' => $file['edited'], 'path' => $path @@ -1999,10 +2021,11 @@ function attach_store_item($channel, $observer, $file) { $post = item_store($arr); - $item_id = $post['item_id']; - - if($item_id) { - Master::Summon(['Notifier', 'activity', $item_id]); + if ($post['success']) { + Master::Summon(['Notifier', 'activity', $post['item_id']]); + if (!empty($post['approval_id'])) { + Master::Summon(['Notifier', 'activity', $post['approval_id']]); + } } } @@ -2581,6 +2604,11 @@ function attach_move($channel_id, $resource_id, $new_folder_hash, $newname = '', } } + if (mb_strlen($filename, 'UTF-8') > MAX_FILENAME_LENGTH) { + logger('filename too long'); + $ret['message'] = t('Filename too long'); + return $ret; + } q("update attach set content = '%s', folder = '%s', filename = '%s', edited = '%s' where id = %d", dbescbin($newstorepath), @@ -2599,33 +2627,31 @@ function attach_move($channel_id, $resource_id, $new_folder_hash, $newname = '', intval($r[0]['id']) ); - if($r[0]['is_photo']) { - q("update photo set album = '%s', filename = '%s', os_path = '%s', display_path = '%s' - where resource_id = '%s' and uid = %d", - dbesc($newalbumname), - dbesc($filename), - dbesc($x['os_path']), - dbesc($x['path']), - dbesc($resource_id), - intval($channel_id) - ); - - q("update photo set content = CASE imgscale WHEN 0 THEN %s ELSE CONCAT(%s, '-', imgscale) END where resource_id = '%s' and uid = %d and os_storage = 1", - dbescbin($newstorepath), - dbescbin($newstorepath), - dbesc($resource_id), - intval($channel_id) - ); - - // now rename the thumbnails in os_storage - the original should have been copied before already - $ps = q("SELECT content, imgscale FROM photo WHERE uid = %d AND resource_id = '%s' and imgscale > 0 and os_storage = 1", + if ($r[0]['is_photo']) { + // update the photo DB entries and copy the thumbnails + $ps = q("SELECT imgscale FROM photo WHERE uid = %d AND resource_id = '%s' and os_storage = 1", intval($channel_id), dbesc($resource_id) ); if ($recurse) { foreach($ps as $p) { - rename($oldstorepath . '-' . $p['imgscale'], $p['content']); + q("update photo set album = '%s', filename = '%s', os_path = '%s', display_path = '%s', content = '%s' + where resource_id = '%s' and imgscale = %d and uid = %d", + dbesc($newalbumname), + dbesc($filename), + dbesc($x['os_path']), + dbesc($x['path']), + dbescbin($newstorepath . ((intval($p['imgscale']) > 0) ? '-' . $p['imgscale'] : '')), + dbesc($resource_id), + intval($p['imgscale']), + intval($channel_id) + ); + + // the original should have been copied already + if (intval($p['imgscale']) > 0) { + rename($oldstorepath . '-' . $p['imgscale'], $newstorepath . '-' . $p['imgscale']); + } } } } @@ -2935,41 +2961,6 @@ function attach_syspaths($channel_id,$attach_hash) { return [ 'os_path' => $os_path, 'path' => $path ]; } -/** - * in earlier releases we did not fill in os_path and display_path in the attach DB structure. - * (It was not needed or used). Going forward we intend to make use of these fields. - * A cron task checks for empty values (as older attachments may have arrived at our site - * in a clone operation) and executes attach_syspaths() to generate these field values and correct - * the attach table entry. The operation is limited to 100 DB entries at a time so as not to - * overload the system in any cron run. Eventually it will catch up with old attach structures - * and switch into maintenance mode to correct any that might arrive in clone packets from older - * sites. - */ - - - -function attach_upgrade() { - $r = q("SELECT id, uid, hash FROM attach WHERE os_path = '' OR display_path = '' LIMIT 100"); - if($r) { - foreach($r as $rv) { - $x = attach_syspaths($rv['uid'],$rv['hash']); - if($x) { - q("update attach set os_path = '%s', display_path = '%s' where id = %d", - dbesc($x['os_path']), - dbesc($x['path']), - intval($rv['id']) - ); - q("update photo set os_path = '%s', display_path = '%s' where uid = %d and resource_id = '%s'", - dbesc($x['os_path']), - dbesc($x['path']), - intval($rv['uid']), - dbesc($rv['hash']) - ); - } - } - } -} - /** * Chunked uploader for integration with the blueimp jquery-uploader diff --git a/include/auth.php b/include/auth.php index 1fc2cc556..36a9043ce 100644 --- a/include/auth.php +++ b/include/auth.php @@ -216,12 +216,11 @@ function requires_mfa_check(int $account_id, string $module, string $arg): bool * also handles logout */ -if((isset($_SESSION)) && (x($_SESSION, 'authenticated')) && - ((! (x($_POST, 'auth-params'))) || ($_POST['auth-params'] !== 'login'))) { +if(!empty($_SESSION['authenticated']) && (empty($_POST['auth-params']) || $_POST['auth-params'] !== 'login')) { // process a logout request - if(((x($_POST, 'auth-params')) && ($_POST['auth-params'] === 'logout')) || (App::$module === 'logout')) { + if((!empty($_POST['auth-params']) && $_POST['auth-params'] === 'logout') || App::$module === 'logout') { // process logout request $args = array('channel_id' => local_channel()); call_hooks('logging_out', $args); @@ -241,7 +240,7 @@ if((isset($_SESSION)) && (x($_SESSION, 'authenticated')) && // re-validate a visitor, optionally invoke "su" if permitted to do so - if(x($_SESSION, 'visitor_id') && (! x($_SESSION, 'uid'))) { + if(!empty($_SESSION['visitor_id']) && empty($_SESSION['uid'])) { // if our authenticated guest is allowed to take control of the admin channel, make it so. $admins = Config::Get('system', 'remote_admin'); if($admins && is_array($admins) && in_array($_SESSION['visitor_id'], $admins)) { @@ -281,7 +280,7 @@ if((isset($_SESSION)) && (x($_SESSION, 'authenticated')) && // already logged in user returning - if(x($_SESSION, 'uid') || x($_SESSION, 'account_id')) { + if(!empty($_SESSION['uid']) || !empty($_SESSION['account_id'])) { App::$session->return_check(); @@ -292,7 +291,7 @@ if((isset($_SESSION)) && (x($_SESSION, 'authenticated')) && if(($r) && (($r[0]['account_flags'] == ACCOUNT_OK) || ($r[0]['account_flags'] == ACCOUNT_UNVERIFIED))) { App::$account = $r[0]; $login_refresh = false; - if(! x($_SESSION,'last_login_date')) { + if(empty($_SESSION['last_login_date'])) { $_SESSION['last_login_date'] = datetime_convert('UTC','UTC'); } if(strcmp(datetime_convert('UTC','UTC','now - 12 hours'), $_SESSION['last_login_date']) > 0 ) { @@ -331,7 +330,7 @@ else { if($password) $encrypted = hash('whirlpool', trim($password)); - if((x($_POST, 'auth-params')) && $_POST['auth-params'] === 'login') { + if(!empty($_POST['auth-params']) && $_POST['auth-params'] === 'login') { $atoken = null; $account = null; @@ -354,9 +353,6 @@ else { elseif($atoken) { atoken_login($atoken); } - else { - notice( t('Failed authentication') . EOL); - } if(! ($account || $atoken)) { $error = 'authenticate: failed login attempt: ' . notags(trim($username)) . ' from IP ' . $_SERVER['REMOTE_ADDR']; @@ -365,8 +361,8 @@ else { $authlog = Config::Get('system', 'authlog'); if ($authlog) @file_put_contents($authlog, datetime_convert() . ':' . session_id() . ' ' . $error . "\n", FILE_APPEND); - notice( t('Login failed.') . EOL ); - goaway(z_root() . '/login'); + + goaway(z_root() . '/login?retry=1'); } // If the user specified to remember the authentication, then change the cookie diff --git a/include/bbcode.php b/include/bbcode.php index 15a75ce3f..d5e1bafd9 100644 --- a/include/bbcode.php +++ b/include/bbcode.php @@ -420,12 +420,12 @@ function getAttachmentData($body) { $type = ""; preg_match("/type='(.*?)'/ism", $attributes, $matches); - if (x($matches, 1)) { + if (!empty($matches[1])) { $type = strtolower($matches[1]); } preg_match('/type=\"\;(.*?)\"\;/ism', $attributes, $matches); - if (x($matches, 1)) { + if (!empty($matches[1])) { $type = strtolower($matches[1]); } @@ -442,12 +442,12 @@ function getAttachmentData($body) { } $url = ""; preg_match("/url='(.*?)'/ism", $attributes, $matches); - if (x($matches, 1)) { + if (!empty($matches[1])) { $url = $matches[1]; } preg_match('/url=\"\;(.*?)\"\;/ism', $attributes, $matches); - if (x($matches, 1)) { + if (!empty($matches[1])) { $url = $matches[1]; } @@ -457,12 +457,12 @@ function getAttachmentData($body) { $title = ""; preg_match("/title='(.*?)'/ism", $attributes, $matches); - if (x($matches, 1)) { + if (!empty($matches[1])) { $title = $matches[1]; } preg_match('/title=\"\;(.*?)\"\;/ism', $attributes, $matches); - if (x($matches, 1)) { + if (!empty($matches[1])) { $title = $matches[1]; } if ($title != "") { @@ -473,12 +473,12 @@ function getAttachmentData($body) { $image = ""; preg_match("/image='(.*?)'/ism", $attributes, $matches); - if (x($matches, 1)) { + if (!empty($matches[1])) { $image = $matches[1]; } preg_match('/image=\"\;(.*?)\"\;/ism', $attributes, $matches); - if (x($matches, 1)) { + if (!empty($matches[1])) { $image = $matches[1]; } @@ -488,12 +488,12 @@ function getAttachmentData($body) { $preview = ""; preg_match("/preview='(.*?)'/ism", $attributes, $matches); - if (x($matches, 1)) { + if (!empty($matches[1])) { $preview = $matches[1]; } preg_match('/preview=\"\;(.*?)\"\;/ism', $attributes, $matches); - if (x($matches, 1)) { + if (!empty($matches[1])) { $preview = $matches[1]; } if ($preview != "") { @@ -1126,9 +1126,11 @@ function parseIdentityAwareHTML($Text) { if ($observer) { $s1 = '<span class="bb_observer" title="' . t('Different viewers will see this text differently') . '">'; $s2 = '</span>'; - $obsBaseURL = $observer['xchan_connurl']; - $obsBaseURL = preg_replace("/\/poco\/.*$/", '', $obsBaseURL); - $Text = str_replace('[observer.baseurl]', $obsBaseURL, $Text); + + $parsed = parse_url($observer['xchan_url']); + $observer_base_url = unparse_url($parsed, ['scheme', 'host', 'port']); + + $Text = str_replace('[observer.baseurl]', $observer_base_url, $Text); $Text = str_replace('[observer.url]',$observer['xchan_url'], $Text); $Text = str_replace('[observer.name]',$s1 . $observer['xchan_name'] . $s2, $Text); $Text = str_replace('[observer.address]',$s1 . $observer['xchan_addr'] . $s2, $Text); @@ -1188,7 +1190,7 @@ function bbcode($text, $options = []) { $cache = ((array_key_exists('cache',$options)) ? $options['cache'] : false); $newwin = ((array_key_exists('newwin',$options)) ? $options['newwin'] : true); - $target = (($newwin) ? ' target="_blank" ' : ''); + $target = (($newwin) ? 'target="_blank"' : ''); /** * @hooks bbcode_filter @@ -1311,9 +1313,11 @@ function bbcode($text, $options = []) { if ($observer) { $s1 = '<span class="bb_observer" title="' . t('Different viewers will see this text differently') . '">'; $s2 = '</span>'; - $obsBaseURL = $observer['xchan_connurl']; - $obsBaseURL = preg_replace("/\/poco\/.*$/", '', $obsBaseURL); - $text = str_replace('[observer.baseurl]', $obsBaseURL, $text); + + $parsed = parse_url($observer['xchan_url']); + $observer_base_url = unparse_url($parsed, ['scheme', 'host', 'port']); + + $text = str_replace('[observer.baseurl]', $observer_base_url, $text); $text = str_replace('[observer.url]',$observer['xchan_url'], $text); $text = str_replace('[observer.name]',$s1 . $observer['xchan_name'] . $s2, $text); $text = str_replace('[observer.address]',$s1 . $observer['xchan_addr'] . $s2, $text); @@ -1328,21 +1332,31 @@ function bbcode($text, $options = []) { $text = str_replace('[observer.photo]','', $text); } - - // Perform URL Search - $urlchars = '[a-zA-Z0-9\pL\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,\@\(\)]'; if (strpos($text,'http') !== false) { if($tryoembed) { $text = preg_replace_callback("/([^\]\='".'"'."\;\/]|^|\#\^)(https?\:\/\/$urlchars+)/ismu", 'tryoembed', $text); } - // Is this still desired? - // We already turn naked URLs into links during creation time cleanup_bbcode() + $text = preg_replace("/([^\]\='".'"'."\;\/]|^|\#\^)(https?\:\/\/$urlchars+)/ismu", '$1<a href="$2" ' . $target . ' rel="nofollow noopener">$2</a>', $text); } + // Turn naked geo URIs into clickable links + if (str_contains($text, 'geo:')) { + $text = preg_replace_callback( + '/([^\]\=\'";\/]|^|\#\^)(geo:([A-Za-z0-9:.,;_+\-?&=%]+)(?:\(([^)]+)\))?)/ismu', + function ($matches) { + $before = $matches[1]; + $geo_uri = $matches[2]; + $label = ((!empty($matches[4])) ? '📍' . urldecode($matches[4]) : $geo_uri); + return $before . '<a href="' . htmlspecialchars($geo_uri) . '" target="_blank" rel="nofollow noopener">' . htmlspecialchars($label) . '</a>'; + }, + $text + ); + } + $count = 0; while (strpos($text,'[/share]') !== false && $count < 10) { $text = preg_replace_callback("/\[share(.*?)\](.*?)\[\/share\]/ism", 'bb_ShareAttributes', $text); @@ -1356,23 +1370,23 @@ function bbcode($text, $options = []) { } if (strpos($text,'[/url]') !== false) { - $text = preg_replace("/\#\^\[url\]([$URLSearchString]*)\[\/url\]/ism", '<span class="bookmark-identifier">#^</span><a class="bookmark" href="$1" ' . $target . ' rel="nofollow noopener" >$1</a>', $text); - $text = preg_replace("/\#\^\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '<span class="bookmark-identifier">#^</span><a class="bookmark" href="$1" ' . $target . ' rel="nofollow noopener" >$2</a>', $text); - $text = preg_replace("/\[url\]([$URLSearchString]*)\[\/url\]/ism", '<a href="$1" ' . $target . ' rel="nofollow noopener" >$1</a>', $text); - $text = preg_replace("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '<a href="$1" ' . $target . ' rel="nofollow noopener" >$2</a>', $text); + $text = preg_replace("/\#\^\[url\]([$URLSearchString]*)\[\/url\]/ism", '<span class="bookmark-identifier">#^</span><a class="bookmark" href="$1" ' . $target . ' rel="nofollow noopener">$1</a>', $text); + $text = preg_replace("/\#\^\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '<span class="bookmark-identifier">#^</span><a class="bookmark" href="$1" ' . $target . ' rel="nofollow noopener">$2</a>', $text); + $text = preg_replace("/\[url\]([$URLSearchString]*)\[\/url\]/ism", '<a href="$1" ' . $target . ' rel="nofollow noopener">$1</a>', $text); + $text = preg_replace("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '<a href="$1" ' . $target . ' rel="nofollow noopener">$2</a>', $text); } if (strpos($text,'[/zrl]') !== false) { - $text = preg_replace("/\#\^\[zrl\]([$URLSearchString]*)\[\/zrl\]/ism", '<span class="bookmark-identifier">#^</span><a class="zrl bookmark" href="$1" ' . $target . ' rel="nofollow noopener" >$1</a>', $text); - $text = preg_replace("/\#\^\[zrl\=([$URLSearchString]*)\](.*?)\[\/zrl\]/ism", '<span class="bookmark-identifier">#^</span><a class="zrl bookmark" href="$1" ' . $target . ' rel="nofollow noopener" >$2</a>', $text); - $text = preg_replace("/\[zrl\]([$URLSearchString]*)\[\/zrl\]/ism", '<a class="zrl" href="$1" ' . $target . ' rel="nofollow noopener" >$1</a>', $text); - $text = preg_replace("/\[zrl\=([$URLSearchString]*)\](.*?)\[\/zrl\]/ism", '<a class="zrl" href="$1" ' . $target . ' rel="nofollow noopener" >$2</a>', $text); + $text = preg_replace("/\#\^\[zrl\]([$URLSearchString]*)\[\/zrl\]/ism", '<span class="bookmark-identifier">#^</span><a class="zrl bookmark" href="$1" ' . $target . ' rel="nofollow noopener">$1</a>', $text); + $text = preg_replace("/\#\^\[zrl\=([$URLSearchString]*)\](.*?)\[\/zrl\]/ism", '<span class="bookmark-identifier">#^</span><a class="zrl bookmark" href="$1" ' . $target . ' rel="nofollow noopener">$2</a>', $text); + $text = preg_replace("/\[zrl\]([$URLSearchString]*)\[\/zrl\]/ism", '<a class="zrl" href="$1" ' . $target . ' rel="nofollow noopener">$1</a>', $text); + $text = preg_replace("/\[zrl\=([$URLSearchString]*)\](.*?)\[\/zrl\]/ism", '<a class="zrl" href="$1" ' . $target . ' rel="nofollow noopener">$2</a>', $text); } // Perform MAIL Search if (strpos($text,'[/mail]') !== false) { - $text = preg_replace("/\[mail\]([$MAILSearchString]*)\[\/mail\]/", '<a href="mailto:$1" ' . $target . ' rel="nofollow noopener" >$1</a>', $text); - $text = preg_replace("/\[mail\=([$MAILSearchString]*)\](.*?)\[\/mail\]/", '<a href="mailto:$1" ' . $target . ' rel="nofollow noopener" >$2</a>', $text); + $text = preg_replace("/\[mail\]([$MAILSearchString]*)\[\/mail\]/", '<a href="mailto:$1" ' . $target . ' rel="nofollow noopener">$1</a>', $text); + $text = preg_replace("/\[mail\=([$MAILSearchString]*)\](.*?)\[\/mail\]/", '<a href="mailto:$1" ' . $target . ' rel="nofollow noopener">$2</a>', $text); } @@ -1398,6 +1412,9 @@ function bbcode($text, $options = []) { if (strpos($text,'[b]') !== false) { $text = preg_replace("(\[b\](.*?)\[\/b\])ism", '<strong>$1</strong>', $text); } + if (strpos($text,'[strong]') !== false) { + $text = preg_replace("(\[strong\](.*?)\[\/strong\])ism", '<strong>$1</strong>', $text); + } // Check for Italics text if (strpos($text,'[i]') !== false) { $text = preg_replace("(\[i\](.*?)\[\/i\])ism", '<em>$1</em>', $text); @@ -1730,17 +1747,17 @@ function bbcode($text, $options = []) { // if video couldn't be embedded, link to it instead. if (strpos($text,'[/video]') !== false) { - $text = preg_replace("/\[video\](.*?)\[\/video\]/", '<a href="$1" ' . $target . ' rel="nofollow noopener" >$1</a>', $text); + $text = preg_replace("/\[video\](.*?)\[\/video\]/", '<a href="$1" ' . $target . ' rel="nofollow noopener">$1</a>', $text); } if (strpos($text,'[/audio]') !== false) { - $text = preg_replace("/\[audio\](.*?)\[\/audio\]/", '<a href="$1" ' . $target . ' rel="nofollow noopener" >$1</a>', $text); + $text = preg_replace("/\[audio\](.*?)\[\/audio\]/", '<a href="$1" ' . $target . ' rel="nofollow noopener">$1</a>', $text); } if (strpos($text,'[/zvideo]') !== false) { - $text = preg_replace("/\[zvideo\](.*?)\[\/zvideo\]/", '<a class="zid" href="$1" ' . $target . ' rel="nofollow noopener" >$1</a>', $text); + $text = preg_replace("/\[zvideo\](.*?)\[\/zvideo\]/", '<a class="zid" href="$1" ' . $target . ' rel="nofollow noopener">$1</a>', $text); } if (strpos($text,'[/zaudio]') !== false) { - $text = preg_replace("/\[zaudio\](.*?)\[\/zaudio\]/", '<a class="zid" href="$1" ' . $target . ' rel="nofollow noopener" >$1</a>', $text); + $text = preg_replace("/\[zaudio\](.*?)\[\/zaudio\]/", '<a class="zid" href="$1" ' . $target . ' rel="nofollow noopener">$1</a>', $text); } // oembed tag @@ -1754,7 +1771,7 @@ function bbcode($text, $options = []) { // Summary (e.g. title) is required, earlier revisions only required description (in addition to // start which is always required). Allow desc with a missing summary for compatibility. - if ((x($ev,'desc') || x($ev,'summary')) && x($ev,'dtstart')) { + if ((!empty($ev['desc']) || !empty($ev['summary'])) && !empty($ev['dtstart'])) { $sub = format_event_html($ev); @@ -1806,9 +1823,13 @@ function bbcode($text, $options = []) { $text = preg_replace("/\<(.*?)(src|href)=(.*?)\&\;(.*?)\>/ism", '<$1$2=$3&$4>', $text); // This is subtle - it's an XSS filter. It only accepts links with a protocol scheme and where - // the scheme begins with z (zhttp), h (http(s)), f (ftp(s)), m (mailto), t (tel) and named anchors. + // the scheme begins with http:, https:, mailto:, tel:, geo: and named anchors. - $text = preg_replace("/\<(.*?)(src|href)=\"[^zhfmt#](.*?)\>/ism", '<$1$2="">', $text); + $text = preg_replace( + '/(<[^>]*?\b(?:src|href)\s*=\s*([\'"])\s*)(?!https?:|geo:|mailto:|tel:|#)[^\'"]*?\2/iu', + '$1$2$2', + $text + ); $text = bb_replace_images($text, $saved_images); diff --git a/include/channel.php b/include/channel.php index 22b5bcde1..fe67f5304 100644 --- a/include/channel.php +++ b/include/channel.php @@ -94,8 +94,10 @@ function validate_channelname($name) { */ call_hooks('validate_channelname', $arr); - if (x($arr, 'message')) + if (!empty($arr['message'])) return $arr['message']; + + return null; } @@ -217,8 +219,8 @@ function create_identity($arr) { } $name = escape_tags($arr['name']); - $pageflags = ((x($arr,'pageflags')) ? intval($arr['pageflags']) : PAGE_NORMAL); - $system = ((x($arr,'system')) ? intval($arr['system']) : 0); + $pageflags = ((!empty($arr['pageflags'])) ? intval($arr['pageflags']) : PAGE_NORMAL); + $system = ((!empty($arr['system'])) ? intval($arr['system']) : 0); $name_error = validate_channelname($arr['name']); if($name_error) { $ret['message'] = $name_error; @@ -1632,19 +1634,19 @@ function profile_sidebar($profile, $block = 0, $show_connect = true, $details = $connect_url = z_root() . '/connect/' . $profile['channel_address']; } - if((x($profile,'address') == 1) - || (x($profile,'locality') == 1) - || (x($profile,'region') == 1) - || (x($profile,'postal_code') == 1) - || (x($profile,'country_name') == 1)) + if(!empty($profile['address']) + || !empty($profile['locality']) + || !empty($profile['region']) + || !empty($profile['postal_code']) + || !empty($profile['country_name'])) $location = t('Location:'); $profile['homepage'] = linkify($profile['homepage'],true); - $gender = ((x($profile,'gender') == 1) ? t('Gender:') : False); - $marital = ((x($profile,'marital') == 1) ? t('Status:') : False); - $homepage = ((x($profile,'homepage') == 1) ? t('Homepage:') : False); - $hometown = ((x($profile,'hometown') == 1) ? t('Hometown:') : False); + $gender = ((!empty($profile['gender'])) ? t('Gender:') : False); + $marital = ((!empty($profile['marital'])) ? t('Status:') : False); + $homepage = ((!empty($profile['homepage'])) ? t('Homepage:') : False); + $hometown = ((!empty($profile['hometown'])) ? t('Hometown:') : False); $profile['online'] = (($profile['online_status'] === 'online') ? t('Online Now') : False); // logger('online: ' . $profile['online']); @@ -2428,7 +2430,7 @@ function get_zcard($channel, $observer_hash = '', $args = array()) { $cover = [ 'href' => z_root() . '/images/default_cover_photos/' . $default_cover . '/' . $cover_width . '.png' ]; } - $o .= replace_macros(get_markup_template('zcard.tpl'), array( + return replace_macros(get_markup_template('zcard.tpl'), array( '$maxwidth' => $maxwidth, '$scale' => $scale, '$translate' => $translate, @@ -2437,8 +2439,6 @@ function get_zcard($channel, $observer_hash = '', $args = array()) { '$pphoto' => $pphoto, '$zcard' => $zcard )); - - return $o; } @@ -2505,17 +2505,13 @@ function get_zcard_embed($channel, $observer_hash = '', $args = array()) { $cover = [ 'href' => z_root() . '/images/default_cover_photos/' . $default_cover . '/' . $cover_width . '.png' ]; } - $o .= replace_macros(get_markup_template('zcard_embed.tpl'),array( + return replace_macros(get_markup_template('zcard_embed.tpl'),array( '$maxwidth' => $maxwidth, - '$scale' => $scale, - '$translate' => $translate, '$size' => $size, '$cover' => $cover, '$pphoto' => $pphoto, '$zcard' => $zcard )); - - return $o; } /** @@ -3106,7 +3102,7 @@ function pchan_to_chan($pchan) { } function channel_url($channel) { - return (($channel) ? z_root() . '/channel/' . $channel['channel_address'] : z_root()); + return ((isset($channel['channel_address'])) ? z_root() . '/channel/' . $channel['channel_address'] : z_root()); } function get_channel_hashes() { diff --git a/include/config.php b/include/config.php index 4dd40eccf..50fe60eb0 100644 --- a/include/config.php +++ b/include/config.php @@ -39,8 +39,6 @@ use Zotlabs\Lib as Zlib; * * @param string $family The category of the configuration value * - * @return Nothing - * * @deprecated * This function is deprecated, use Zotlabs\Lib\Config::Load * instead. diff --git a/include/connections.php b/include/connections.php index efc531171..f0ea8583b 100644 --- a/include/connections.php +++ b/include/connections.php @@ -86,7 +86,7 @@ function deliverable_abook_xchans($channel_id, $filter = [], $flatten = true) { $r = q("SELECT abook_xchan, xchan_network FROM abook LEFT JOIN xchan ON abook_xchan = xchan_hash WHERE abook_channel = %d $filter_sql AND abook_self = 0 - AND abook_pending = 0 + AND abook_blocked = 0 AND abook_archived = 0 AND abook_not_here = 0 AND xchan_network NOT IN ('anon', 'token', 'rss')", @@ -342,7 +342,7 @@ function remove_all_xchan_resources($xchan, $channel_id = 0) { if($r) { foreach($r as $rr) { - drop_item($rr['id'],false); + drop_item($rr['id']); } } @@ -513,7 +513,7 @@ function remove_abook_items($channel_id, $xchan_hash) { continue; } - drop_item($rr['id'],false); + drop_item($rr['id'], uid: $channel_id); } } diff --git a/include/conversation.php b/include/conversation.php index c631d53a2..07e4df088 100644 --- a/include/conversation.php +++ b/include/conversation.php @@ -3,6 +3,7 @@ use Zotlabs\Lib\Activity; use Zotlabs\Lib\Apps; use Zotlabs\Lib\Config; +use Zotlabs\Lib\PConfig; require_once('include/items.php'); @@ -176,9 +177,7 @@ function localize_item(&$item){ case ACTIVITY_OBJ_NOTE: case 'Note': default: - $post_type = t('post'); - if(((isset($obj['parent']) && isset($obj['id']) && $obj['id'] != $obj['parent'])) || isset($obj['inReplyTo'])) - $post_type = t('comment'); + $post_type = t('message'); break; } @@ -346,20 +345,20 @@ function conversation($items, $mode, $update, $page_mode = 'traditional', $prepa . "<script> var profile_uid = " . $_SESSION['uid'] . "; var netargs = '" . substr(App::$cmd,8) . '?f=' - . ((x($_GET,'cid')) ? '&cid=' . $_GET['cid'] : '') - . ((x($_GET,'search')) ? '&search=' . $_GET['search'] : '') - . ((x($_GET,'star')) ? '&star=' . $_GET['star'] : '') - . ((x($_GET,'order')) ? '&order=' . $_GET['order'] : '') - . ((x($_GET,'bmark')) ? '&bmark=' . $_GET['bmark'] : '') - . ((x($_GET,'liked')) ? '&liked=' . $_GET['liked'] : '') - . ((x($_GET,'conv')) ? '&conv=' . $_GET['conv'] : '') - . ((x($_GET,'spam')) ? '&spam=' . $_GET['spam'] : '') - . ((x($_GET,'nets')) ? '&nets=' . $_GET['nets'] : '') - . ((x($_GET,'cmin')) ? '&cmin=' . $_GET['cmin'] : '') - . ((x($_GET,'cmax')) ? '&cmax=' . $_GET['cmax'] : '') - . ((x($_GET,'file')) ? '&file=' . $_GET['file'] : '') - . ((x($_GET,'uri')) ? '&uri=' . $_GET['uri'] : '') - . ((x($_GET,'pf')) ? '&pf=' . $_GET['pf'] : '') + . (!empty($_GET['cid']) ? '&cid=' . $_GET['cid'] : '') + . (!empty($_GET['search']) ? '&search=' . $_GET['search'] : '') + . (!empty($_GET['star']) ? '&star=' . $_GET['star'] : '') + . (!empty($_GET['order']) ? '&order=' . $_GET['order'] : '') + . (!empty($_GET['bmark']) ? '&bmark=' . $_GET['bmark'] : '') + . (!empty($_GET['liked']) ? '&liked=' . $_GET['liked'] : '') + . (!empty($_GET['conv']) ? '&conv=' . $_GET['conv'] : '') + . (!empty($_GET['spam']) ? '&spam=' . $_GET['spam'] : '') + . (!empty($_GET['nets']) ? '&nets=' . $_GET['nets'] : '') + . (!empty($_GET['cmin']) ? '&cmin=' . $_GET['cmin'] : '') + . (!empty($_GET['cmax']) ? '&cmax=' . $_GET['cmax'] : '') + . (!empty($_GET['file']) ? '&file=' . $_GET['file'] : '') + . (!empty($_GET['uri']) ? '&uri=' . $_GET['uri'] : '') + . (!empty($_GET['pf']) ? '&pf=' . $_GET['pf'] : '') . "'; var profile_page = " . App::$pager['page'] . "; </script>\r\n"; } } @@ -438,17 +437,22 @@ function conversation($items, $mode, $update, $page_mode = 'traditional', $prepa $uploading = false; - if(local_channel()) { - $cur_channel = App::get_channel(); - if($cur_channel['channel_allow_cid'] === '' && $cur_channel['channel_allow_gid'] === '' - && $cur_channel['channel_deny_cid'] === '' && $cur_channel['channel_deny_gid'] === '' + $channel = App::get_channel(); + $observer = App::get_observer(); + + if (local_channel()) { + // Allow uploading if there is no default privacy and the view_storage permission is set to PERMS_PUBLIC + if ($channel['channel_allow_cid'] === '' && $channel['channel_allow_gid'] === '' + && $channel['channel_deny_cid'] === '' && $channel['channel_deny_gid'] === '' && intval(\Zotlabs\Access\PermissionLimits::Get(local_channel(),'view_storage')) === PERMS_PUBLIC) { $uploading = true; } - } - $channel = App::get_channel(); - $observer = App::get_observer(); + // Allow uploading if OCAP tokens are enabled + if (PConfig::Get(local_channel(), 'system', 'ocap_enabled')) { + $uploading = true; + } + } if (!$update) { $_SESSION['return_url'] = App::$query_string; @@ -461,17 +465,6 @@ function conversation($items, $mode, $update, $page_mode = 'traditional', $prepa $items = $cb['items']; - $conv_responses = [ - 'like' => ['title' => t('Likes','title')], - 'dislike' => ['title' => t('Dislikes','title')], - 'attendyes' => ['title' => t('Attending','title')], - 'attendno' => ['title' => t('Not attending','title')], - 'attendmaybe' => ['title' => t('Might attend','title')], - 'answer' => [], - 'announce' => ['title' => t('Repeats','title')], - ]; - - // array with html for each thread (parent+comments) $threads = array(); $threadsid = -1; @@ -560,7 +553,7 @@ function conversation($items, $mode, $update, $page_mode = 'traditional', $prepa $shareable = false; $verified = (intval($item['item_verified']) ? t('Message signature validated') : ''); - $forged = ((($item['sig']) && (! intval($item['item_verified']))) ? t('Message signature incorrect') : ''); + $forged = ((!empty($item['sig']) && !intval($item['item_verified'])) ? t('Message signature incorrect') : ''); $unverified = ''; @@ -698,14 +691,10 @@ function conversation($items, $mode, $update, $page_mode = 'traditional', $prepa $item = $x['item']; - builtin_activity_puller($item, $conv_responses); - - if(! visible_activity($item)) { + if (!visible_activity($item)) { continue; } - $mid_uuid_map[$item['mid']] = $item['uuid']; - $item['pagedrop'] = $page_dropping; if($item['id'] == $item['parent'] || $r_preview) { @@ -714,7 +703,6 @@ function conversation($items, $mode, $update, $page_mode = 'traditional', $prepa $conv->add_thread($item_object); if(($page_mode === 'list') || ($page_mode === 'pager_list')) { - $item_object->set_template('conv_list.tpl'); $item_object->set_display_mode('list'); } if($mode === 'cards' || $mode === 'articles') { @@ -724,7 +712,7 @@ function conversation($items, $mode, $update, $page_mode = 'traditional', $prepa } } - $threads = $conv->get_template_data($conv_responses, $mid_uuid_map); + $threads = $conv->get_template_data(); if(!$threads) { logger('[ERROR] conversation : Failed to get template data.', LOGGER_DEBUG); $threads = array(); @@ -775,7 +763,7 @@ function best_link_url($item) { $clean_url = isset($item['author-link']) ? normalise_link($item['author-link']) : ''; if($clean_url && local_channel() && (local_channel() == $item['uid'])) { - if(isset(App::$contacts) && x(App::$contacts, $clean_url)) { + if(isset(App::$contacts) && !empty(App::$contacts[$clean_url])) { if(App::$contacts[$clean_url]['network'] === NETWORK_DFRN) { $best_url = z_root() . '/redir/' . App::$contacts[$clean_url]['id']; $sparkle = true; @@ -817,7 +805,7 @@ function thread_action_menu($item,$mode = '') { $menu[] = [ 'menu' => 'unfollow_thread', 'title' => t('Unfollow Thread'), - 'icon' => 'minus', + 'icon' => 'dash', 'action' => 'dounsubthread(' . $item['id'] . '); return false;', 'href' => '#' ]; @@ -946,101 +934,6 @@ function thread_author_menu($item, $mode = '') { } - - - -/** - * @brief Checks item to see if it is one of the builtin activities (like/dislike, event attendance, consensus items, etc.) - * - * Increments the count of each matching activity and adds a link to the author as needed. - * - * @param array $item - * @param array &$conv_responses (already created with builtin activity structure) - */ -function builtin_activity_puller($item, &$conv_responses) { - - // if this item is a post or comment there's nothing for us to do here, just return. - - if(activity_match($item['verb'], ['Create', ACTIVITY_POST]) && $item['obj_type'] !== 'Answer') - return; - - foreach($conv_responses as $mode => $v) { - - $url = ''; - - switch($mode) { - case 'like': - $verb = ['Like', ACTIVITY_LIKE]; - break; - case 'dislike': - $verb = ['Dislike', ACTIVITY_DISLIKE]; - break; - case 'attendyes': - $verb = ['Accept', ACTIVITY_ATTEND]; - break; - case 'attendno': - $verb = ['Reject', ACTIVITY_ATTENDNO]; - break; - case 'attendmaybe': - $verb = ['TentativeAccept', ACTIVITY_ATTENDMAYBE]; - break; - case 'answer': - $verb = ['Create', ACTIVITY_POST]; - break; - case 'announce': - $verb = 'Announce'; - break; - default: - return; - break; - } - - if((activity_match($item['verb'], $verb)) && ($item['id'] != $item['parent'])) { - - $name = (($item['author']['xchan_name']) ? $item['author']['xchan_name'] : t('Unknown')); - - $moderate = ((intval($item['item_blocked']) === ITEM_MODERATED) ? '<a href="moderate/' . $item['id'] . '/approve" onclick="moderate_approve(' . $item['id'] . '); return false;" class="text-success pe-2" title="' . t('Approve this item') . '"><i class="fa fa-check" ></i></a><a href="moderate/' . $item['id'] . '/drop" onclick="moderate_drop(' . $item['id'] . '); return false;" class="text-danger pe-2" title="' . t('Delete this item') . '"><i class="fa fa-trash-o" ></i></a>' : ''); - - $url = (($item['author_xchan'] && $item['author']['xchan_photo_s']) - ? '<div class="dropdown-item">' . $moderate . '<a href="' . chanlink_hash($item['author_xchan']) . '" class="text-reset">' . '<img class="menu-img-1" src="' . zid($item['author']['xchan_photo_s']) . '" alt="' . urlencode($name) . '" /> ' . $name . '</a></div>' - : '<a class="dropdown-item" href="#" class="disabled">' . $name . '</a>' - ); - - - - if(! $item['thr_parent']) - $item['thr_parent'] = $item['parent_mid']; - - $conv_responses[$mode]['mids'][$item['thr_parent']][] = $item['uuid']; - - if($item['obj_type'] === 'Answer') - continue; - - if(! ((isset($conv_responses[$mode][$item['thr_parent'] . '-l'])) - && (is_array($conv_responses[$mode][$item['thr_parent'] . '-l'])))) - $conv_responses[$mode][$item['thr_parent'] . '-l'] = array(); - - // only list each unique author once - if(in_array($url,$conv_responses[$mode][$item['thr_parent'] . '-l'])) - continue; - - if(! isset($conv_responses[$mode][$item['thr_parent']])) - $conv_responses[$mode][$item['thr_parent']] = 1; - else - $conv_responses[$mode][$item['thr_parent']] ++; - - $conv_responses[$mode][$item['thr_parent'] . '-l'][] = $url; - if(get_observer_hash() && get_observer_hash() === $item['author_xchan']) { - $conv_responses[$mode][$item['thr_parent'] . '-m'] = true; - } - - // there can only be one activity verb per item so if we found anything, we can stop looking - return; - } - } -} - - /** * @brief Format the like/dislike text for a profile item. * @@ -1098,54 +991,52 @@ function status_editor($x, $popup = false, $module='') { function hz_status_editor($x, $popup = false) { - $o = ''; - $c = channelx_by_n($x['profile_uid']); if($c && $c['channel_moved']) - return $o; + return; - $webpage = ((x($x,'webpage')) ? $x['webpage'] : ''); + $webpage = ((!empty($x['webpage'])) ? $x['webpage'] : ''); $plaintext = true; $feature_nocomment = feature_enabled($x['profile_uid'], 'disable_comments'); - if(x($x, 'disable_comments')) + if(!empty($x['disable_comments'])) $feature_nocomment = false; $feature_expire = ((feature_enabled($x['profile_uid'], 'content_expire') && (! $webpage)) ? true : false); - if(x($x, 'hide_expire')) + if(!empty($x['hide_expire'])) $feature_expire = false; $feature_future = ((feature_enabled($x['profile_uid'], 'delayed_posting') && (! $webpage)) ? true : false); - if(x($x, 'hide_future')) + if(!empty($x['hide_future'])) $feature_future = false; $geotag = ((isset($x['allow_location']) && $x['allow_location']) ? replace_macros(get_markup_template('jot_geotag.tpl'), array()) : ''); $setloc = t('Set your location'); $clearloc = ((get_pconfig($x['profile_uid'], 'system', 'use_browser_location')) ? t('Clear browser location') : ''); - if(x($x, 'hide_location')) + if(!empty($x['hide_location'])) $geotag = $setloc = $clearloc = ''; - $mimetype = ((x($x,'mimetype')) ? $x['mimetype'] : 'text/bbcode'); + $mimetype = ((!empty($x['mimetype'])) ? $x['mimetype'] : 'text/bbcode'); - $mimeselect = ((x($x,'mimeselect')) ? $x['mimeselect'] : false); + $mimeselect = ((!empty($x['mimeselect'])) ? $x['mimeselect'] : false); if($mimeselect) $mimeselect = mimetype_select($x['profile_uid'], $mimetype); else $mimeselect = '<input type="hidden" name="mimetype" value="' . $mimetype . '" />'; $weblink = (($mimetype === 'text/bbcode') ? t('Insert web link') : false); - if(x($x, 'hide_weblink')) + if(!empty($x['hide_weblink'])) $weblink = false; $embedPhotos = t('Embed (existing) photo from your photo albums'); $writefiles = (($mimetype === 'text/bbcode') ? perm_is_allowed($x['profile_uid'], get_observer_hash(), 'write_storage') : false); - if(x($x, 'hide_attach')) + if(!empty($x['hide_attach'])) $writefiles = false; - $layout = ((x($x,'layout')) ? $x['layout'] : ''); + $layout = ((!empty($x['layout'])) ? $x['layout'] : ''); - $layoutselect = ((x($x,'layoutselect')) ? $x['layoutselect'] : false); + $layoutselect = ((!empty($x['layoutselect'])) ? $x['layoutselect'] : false); if($layoutselect) $layoutselect = layout_select($x['profile_uid'], $layout); else @@ -1158,7 +1049,7 @@ function hz_status_editor($x, $popup = false) { else $id_select = ''; - $reset = ((x($x,'reset')) ? $x['reset'] : ''); + $reset = ((!empty($x['reset'])) ? $x['reset'] : ''); $feature_auto_save_draft = ((feature_enabled($x['profile_uid'], 'auto_save_draft')) ? "true" : "false"); @@ -1167,14 +1058,14 @@ function hz_status_editor($x, $popup = false) { $tplmacros = [ '$baseurl' => z_root(), '$editselect' => (($plaintext) ? 'none' : '/(profile-jot-text|prvmail-text)/'), - '$pretext' => ((x($x,'pretext')) ? $x['pretext'] : ''), + '$pretext' => ((!empty($x['pretext'])) ? $x['pretext'] : ''), '$geotag' => $geotag, '$nickname' => $x['nickname'], '$linkurl' => t('Please enter a link URL:'), '$term' => t('Tag term:'), '$whereareu' => t('Where are you right now?'), - '$editor_autocomplete'=> ((x($x,'editor_autocomplete')) ? $x['editor_autocomplete'] : ''), - '$bbco_autocomplete'=> ((x($x,'bbco_autocomplete')) ? $x['bbco_autocomplete'] : ''), + '$editor_autocomplete'=> ((!empty($x['editor_autocomplete'])) ? $x['editor_autocomplete'] : ''), + '$bbco_autocomplete'=> ((!empty($x['bbco_autocomplete'])) ? $x['bbco_autocomplete'] : ''), '$modalchooseimages' => t('Choose images to embed'), '$modalchoosealbum' => t('Choose an album'), '$modaldiffalbum' => t('Choose a different album...'), @@ -1201,7 +1092,7 @@ function hz_status_editor($x, $popup = false) { $tpl = get_markup_template('jot.tpl'); $preview = t('Preview'); - if(x($x, 'hide_preview')) + if(!empty($x['hide_preview'])) $preview = ''; $defexpire = ((($z = get_pconfig($x['profile_uid'], 'system', 'default_post_expire')) && (! $webpage)) ? $z : ''); @@ -1229,26 +1120,27 @@ function hz_status_editor($x, $popup = false) { call_hooks('jot_tool', $jotplugins); $jotnets = ''; - if(x($x,'jotnets')) { + if(!empty($x['jotnets'])) { call_hooks('jot_networks', $jotnets); } - $sharebutton = (x($x,'button') ? $x['button'] : t('Share')); - $placeholdtext = (x($x,'content_label') ? $x['content_label'] : $sharebutton); + $sharebutton = (!empty($x['button']) ? $x['button'] : t('Submit')); + $placeholdtext = (!empty($x['content_label']) ? $x['content_label'] : t('Start a conversation')); $tplmacros = [ - '$return_path' => ((x($x, 'return_path')) ? $x['return_path'] : App::$query_string), + '$return_path' => ((!empty($x['return_path'])) ? $x['return_path'] : App::$query_string), '$action' => z_root() . '/item', '$share' => $sharebutton, '$placeholdtext' => $placeholdtext, '$webpage' => $webpage, - '$placeholdpagetitle' => ((x($x,'ptlabel')) ? $x['ptlabel'] : t('Page link name')), - '$pagetitle' => (x($x,'pagetitle') ? $x['pagetitle'] : ''), + '$placeholdpagetitle' => ((!empty($x['ptlabel'])) ? $x['ptlabel'] : t('Page link name')), + '$pagetitle' => (!empty($x['pagetitle']) ? $x['pagetitle'] : ''), '$id_select' => $id_select, '$id_seltext' => t('Post as'), '$writefiles' => $writefiles, '$bold' => t('Bold'), '$italic' => t('Italic'), + '$highlighter' => t('Highlight selected text'), '$underline' => t('Underline'), '$quote' => t('Quote'), '$code' => t('Code'), @@ -1271,18 +1163,18 @@ function hz_status_editor($x, $popup = false) { '$feature_nocomment' => $feature_nocomment, '$nocomment' => ((array_key_exists('item',$x)) ? $x['item']['item_nocomment'] : 0), '$clearloc' => $clearloc, - '$title' => ((x($x, 'title')) ? htmlspecialchars($x['title'], ENT_COMPAT,'UTF-8') : ''), - '$summary' => ((x($x, 'summary')) ? htmlspecialchars($x['summary'], ENT_COMPAT,'UTF-8') : ''), - '$placeholdertitle' => ((x($x, 'placeholdertitle')) ? $x['placeholdertitle'] : t('Title (optional)')), - '$placeholdersummary' => ((x($x, 'placeholdersummary')) ? $x['placeholdersummary'] : t('Summary (optional)')), + '$title' => ((!empty($x['title'])) ? htmlspecialchars($x['title'], ENT_COMPAT,'UTF-8') : ''), + '$summary' => ((!empty($x['summary'])) ? htmlspecialchars($x['summary'], ENT_COMPAT,'UTF-8') : ''), + '$placeholdertitle' => ((!empty($x['placeholdertitle'])) ? $x['placeholdertitle'] : t('Title (optional)')), + '$placeholdersummary' => ((!empty($x['placeholdersummary'])) ? $x['placeholdersummary'] : t('Summary (optional)')), '$catsenabled' => $catsenabled, - '$category' => ((x($x, 'category')) ? $x['category'] : ''), + '$category' => ((!empty($x['category'])) ? $x['category'] : ''), '$placeholdercategory' => t('Categories (optional, comma-separated list)'), '$permset' => t('Permission settings'), - '$ptyp' => ((x($x, 'ptyp')) ? $x['ptyp'] : ''), - '$content' => ((x($x,'body')) ? htmlspecialchars($x['body'], ENT_COMPAT,'UTF-8') : ''), - '$attachment' => ((x($x, 'attachment')) ? $x['attachment'] : ''), - '$post_id' => ((x($x, 'post_id')) ? $x['post_id'] : ''), + '$ptyp' => ((!empty($x['ptyp'])) ? $x['ptyp'] : ''), + '$content' => ((!empty($x['body'])) ? htmlspecialchars($x['body'], ENT_COMPAT,'UTF-8') : ''), + '$attachment' => ((!empty($x['attachment'])) ? $x['attachment'] : ''), + '$post_id' => ((!empty($x['post_id'])) ? $x['post_id'] : ''), '$defloc' => $x['default_location'] ?? '', '$visitor' => $x['visitor'] ?? '', '$lockstate' => $x['lockstate'] ?? '', @@ -1297,7 +1189,7 @@ function hz_status_editor($x, $popup = false) { '$bang' => $x['bang'] ?? '', '$profile_uid' => $x['profile_uid'], '$preview' => $preview, - '$source' => ((x($x, 'source')) ? $x['source'] : ''), + '$source' => ((!empty($x['source'])) ? $x['source'] : ''), '$jotplugins' => $jotplugins, '$jotnets' => $jotnets, '$jotnets_label' => t('Other networks and post services'), @@ -1312,33 +1204,31 @@ function hz_status_editor($x, $popup = false) { '$cipher' => $cipher, '$expiryModalOK' => t('OK'), '$expiryModalCANCEL' => t('Cancel'), - '$expanded' => ((x($x, 'expanded')) ? $x['expanded'] : false), - '$bbcode' => ((x($x, 'bbcode')) ? $x['bbcode'] : false), + '$expanded' => ((!empty($x['expanded'])) ? $x['expanded'] : false), + '$bbcode' => ((!empty($x['bbcode'])) ? $x['bbcode'] : false), '$parent' => ((array_key_exists('parent',$x) && $x['parent']) ? $x['parent'] : 0), '$reset' => $reset, '$is_owner' => ((local_channel() && (local_channel() == $x['profile_uid'])) ? true : false), '$customjotheaders' => '', '$custommoretoolsdropdown' => '', '$custommoretoolsbuttons' => '', - '$customsubmitright' => [] + '$customsubmitright' => [], + '$popup' => $popup ]; call_hooks('jot_tpl_filter',$tplmacros); - $o .= replace_macros($tpl, $tplmacros); - if ($popup === true) { - $o = '<div id="jot-popup" style="display:none">' . $o . '</div>'; - } - - return $o; + return replace_macros($tpl, $tplmacros); } function get_item_children($arr, $parent) { - $children = array(); + $children = []; + $thread_allow = ((local_channel()) ? PConfig::Get(local_channel(), 'system', 'thread_allow', true) : Config::Get('system', 'thread_allow', true)); + foreach($arr as $item) { if($item['id'] != $item['parent']) { - if(Config::Get('system','thread_allow')) { + if ($thread_allow) { // Fallback to parent_mid if thr_parent is not set $thr_parent = $item['thr_parent']; if($thr_parent == '') @@ -1467,14 +1357,18 @@ function render_location_default($item) { $location = $item['location']; $coord = $item['coord']; - if($coord) { + if ($coord) { if($location) - $location .= ' <span class="smalltext">(' . $coord . ')</span>'; + $location .= ' (' . $coord . ')'; else - $location = '<span class="smalltext">' . $coord . '</span>'; + $location = $coord; } - return $location; + if (!$location) { + return ''; + } + + return '<i class="bi bi-geo-alt" title="' . $location . '"></i>'; } @@ -1521,53 +1415,50 @@ function prepare_page($item) { )); } -function get_responses($conv_responses,$response_verbs,$ob,$item) { +function get_responses($response_verbs, $item) { $ret = array(); foreach($response_verbs as $v) { - $ret[$v] = []; - $ret[$v]['count'] = $conv_responses[$v][$item['mid']] ?? 0; - $ret[$v]['list'] = ((isset($conv_responses[$v][$item['mid']])) ? $conv_responses[$v][$item['mid'] . '-l'] : ''); - $ret[$v]['button'] = get_response_button_text($v, $ret[$v]['count']); - $ret[$v]['title'] = $conv_responses[$v]['title'] ?? ''; - $ret[$v]['modal'] = (($ret[$v]['count'] > MAX_LIKERS) ? true : false); - } + if ($v === 'answer') { + // we require the structure to collect the response hashes + // but we do not use them for display - do not collect them. + continue; + } - $count = 0; - foreach ($ret as $key) { - if ($key['count'] == true) - $count++; + $ret[$v]['count'] = $item[$v . '_count'] ?? 0; + $ret[$v]['button'] = get_response_button_text($v, $ret[$v]['count'], $item['item_thread_top']); } - $ret['count'] = $count; - //logger('ret: ' . print_r($ret,true)); return $ret; } -function get_response_button_text($v,$count) { +function get_response_button_text($v, $count = 0, $top_level = 0) { switch($v) { case 'like': - return ['label' => tt('Like','Likes',$count,'noun'), 'icon' => 'thumbs-o-up', 'class' => 'like']; + return ['label' => tt('Like','Likes',$count,'noun'), 'icon' => 'hand-thumbs-up', 'class' => 'like', 'action' => 'dolike']; break; case 'announce': - return ['label' => tt('Repeat','Repeats',$count,'noun'), 'icon' => 'retweet', 'class' => 'announce']; + return ['label' => tt('Repeat','Repeats',$count,'noun'), 'icon' => 'repeat', 'class' => 'announce', 'action' => 'jotShare']; break; case 'dislike': - return ['label' => tt('Dislike','Dislikes',$count,'noun'), 'icon' => 'thumbs-o-down', 'class' => 'dislike']; + return ['label' => tt('Dislike','Dislikes',$count,'noun'), 'icon' => 'hand-thumbs-down', 'class' => 'dislike', 'action' => 'dolike']; + break; + case 'comment': + return ['label' => (($top_level) ? tt('Comment', 'Comments' ,$count, 'noun') : tt('Reply', 'Replies', $count, 'noun')), 'icon' => 'chat', 'class' => 'comment', 'action' => '']; break; - case 'attendyes': - return ['label' => tt('Attending','Attending',$count,'noun'), 'icon' => 'calendar-check-o', 'class' => 'attendyes']; + case 'accept': + return ['label' => tt('Attending','Attending',$count,'noun'), 'icon' => 'calendar-check', 'class' => 'accept', 'action' => 'dolike']; break; - case 'attendno': - return ['label' => tt('Not Attending','Not Attending',$count,'noun'), 'icon' => 'calendar-times-o', 'class' => 'attendno']; + case 'reject': + return ['label' => tt('Not attending','Not attending',$count,'noun'), 'icon' => 'calendar-x', 'class' => 'reject', 'action' => 'dolike']; break; - case 'attendmaybe': - return ['label' => tt('Undecided','Undecided',$count,'noun'), 'icon' => 'calendar-o', 'class' => 'attendmaybe']; + case 'tentativeaccept': + return ['label' => tt('Undecided','Undecided',$count,'noun'), 'icon' => 'calendar', 'class' => 'tentativeaccept', 'action' => 'dolike']; break; default: - return ''; + return []; break; } } diff --git a/include/datetime.php b/include/datetime.php index 4c7105138..89e2876d0 100644 --- a/include/datetime.php +++ b/include/datetime.php @@ -264,6 +264,45 @@ function relative_date($posted_date, $format = null) { return sprintf($format, $r, plural_dates($str,$r)); } } + + return $abs; +} + +/** + * @brief Returns a relative time string like 3 seconds ago. + * @param string $posted_date (UTC) + * @param DateTime $now (optional) + * @return string with relative time + */ +function relative_time($timestamp, $now = new DateTime()) { + $localtime = datetime_convert('UTC', date_default_timezone_get(), $timestamp); + $time = new DateTime($localtime); + + $interval = $now->diff($time); + + $prefix = ''; + $appendix = ' ' . t('ago'); + + if ($time > $now) { + $prefix = t('in') . ' '; + $appendix = ''; + } + + if ($interval->y > 0) { + return $prefix . $interval->y . ' ' . plural_dates('y', $interval->y) . $appendix; + } elseif ($interval->m > 0) { + return $prefix . $interval->m . ' ' . plural_dates('m', $interval->m) . $appendix; + } elseif ($interval->d > 0) { + return $prefix . $interval->d . ' ' . plural_dates('d', $interval->d) . $appendix; + } elseif ($interval->h > 0) { + return $prefix . $interval->h . ' ' . plural_dates('h', $interval->h) . $appendix; + } elseif ($interval->i > 0) { + return $prefix . $interval->i . ' ' . plural_dates('i', $interval->i) . $appendix; + } elseif ($interval->s > 0) { + return $prefix . $interval->s . ' ' . plural_dates('s', $interval->s) . $appendix; + } else { + return t('now'); + } } function plural_dates($k,$n) { @@ -525,7 +564,7 @@ function update_birthdays() { $z = event_store_event($ev); if ($z) { - $item_id = event_store_item($ev, $z); + event_store_item($ev, $z, false); q("update abook set abook_dob = '%s' where abook_id = %d", dbesc(intval($rr['abook_dob']) + 1 . substr($rr['abook_dob'], 4)), intval($rr['abook_id']) diff --git a/include/dba/dba_pdo.php b/include/dba/dba_pdo.php index c8a1b6c85..3b0edefcd 100644 --- a/include/dba/dba_pdo.php +++ b/include/dba/dba_pdo.php @@ -10,6 +10,8 @@ class dba_pdo extends dba_driver { public $driver_dbtype = null; + private string $server_version = ''; + /** * {@inheritDoc} * @see dba_driver::connect() @@ -37,6 +39,7 @@ class dba_pdo extends dba_driver { try { $this->db = new PDO($dsn,$user,$pass); $this->db->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION); + $this->server_version = $this->db->getAttribute(PDO::ATTR_SERVER_VERSION); } catch(PDOException $e) { if(file_exists('dbfail.out')) { @@ -73,9 +76,9 @@ class dba_pdo extends dba_driver { } } - $result = null; + $result = false; $this->error = ''; - $select = ((stripos($sql, 'select') === 0) ? true : false); + $select = stripos($sql, 'select') === 0 || stripos($sql, 'with') === 0 || stripos($sql, 'returning ') > 0; try { $result = $this->db->query($sql, PDO::FETCH_ASSOC); @@ -115,6 +118,111 @@ class dba_pdo extends dba_driver { return (($this->error) ? false : $r); } + /** + * Insert a row into a table. + * + * The `$data` argument is an array of key/value pairs of the columns to + * insert, where the key is the column name. Values are automatically + * escaped if needed, and should be provided unescaped to this function. + * + * @note it is the callers responsibility to ensure that only valid + * column names are passed as keys in the array. + * + * The inserted row will be returned. + * + * @param string $table The table to insert the row into. + * @param array $data The data to insert as an array of column name => value pairs. + * @param string $idcol The column name for the primary key of the table. We need to + * specify this since we don't have a consistent naming of primary + * id for tables. + * + * @return array|bool The complete record as read back from the database, or false if we + * could not fetch it. + */ + public function insert(string $table, array $data, string $idcol): array|bool { + $keys = array_keys($data); + $values = array_map( + fn ($v) => is_numeric($v) ? $v : "'" . dbesc($v) . "'", + array_values($data) + ); + + $query = "INSERT INTO {$table} (" + . implode(', ', $keys) . ') VALUES (' + . implode(', ', $values) . ')'; + + // MySQL is the only supported DB that don't support the returning + // clause. Since the driver type is 'mysql' also for MariaDB, we need + // to check the actual server version to be sure we only exclude actual + // MySQL systems. + if ($this->driver_dbtype !== 'mysql' || stripos($this->server_version, 'mariadb') !== false) { + $query .= ' RETURNING *'; + } + + $res = $this->q($query); + + if (is_a($res, PDOStatement::class)) { + // + // Calling PDO::lastInsertId should be safe here. + // The last inserted id is kept for each connection, so we're not risking + // a race condition wrt inserts by other requests that happen simultaneously. + // + $id = $this->db->lastInsertId($table); + + $res = $this->q("SELECT * FROM {$table} WHERE {$idcol} = {$id}"); + + if (is_a($res, PDOStatement::class)) { + db_logger('dba_pdo: PDOStatement returned, did not expect that.'); + return false; + } + } + + if (is_array($res)) { + // Since we should never have more than one result, unwrap the array + // so we only have the resulting row. + $res = $res[0]; + } + + return $res; + } + + /** + * Update an existing row in a table. + * + * The `$data` argument is an array of key/value pairs of the columns to + * update, where the key is the column name. Values are automatically + * escaped if needed, and should be provided unescaped to this function. + * + * @note it is the callers responsibility to ensure that only valid + * column names are passed as keys in the array. + * + * The row to be updated is identified by `$idcol` and `$idval` as the + * column name and value respectively. This should normally be the unique + * id column of the table, but can in theory be any column with a unique + * value that identifies a specific row. + * + * @param string $table The table to update. + * @param array $data The columns to update as key => value pairs. + * @param string $idcol The name of the id column to check $idval against. + * @param mixed $idval The id of the row to update. + * + * @return bool True if the update succeeded, false otherwise. + */ + public function update(string $table, array $data, string $idcol, mixed $idval): bool { + $set_statements = []; + + foreach ($data as $k => $v) { + $set_statements[] = "set {$k}=" . (is_numeric($v) ? $v : "'" . dbesc($v) . "'"); + } + + $query = "UPDATE {$table} " + . implode(', ', $set_statements) + . " WHERE {$idcol} = {$idval}"; + + $res = $this->q($query); + + return is_a($res, PDOStatement::class); + } + function escape($str) { if($this->db && $this->connected) { return substr(substr(@$this->db->quote($str),1),0,-1); diff --git a/include/event.php b/include/event.php index f8511cbe2..39d0c49c2 100644 --- a/include/event.php +++ b/include/event.php @@ -13,7 +13,7 @@ use Zotlabs\Lib\Libsync; use Zotlabs\Access\AccessList; use Ramsey\Uuid\Uuid; -use Ramsey\Uuid\Exception\UnsatisfiedDependencyException; +use Ramsey\Uuid\Exception\UnableToBuildUuidException; require_once('include/bbcode.php'); @@ -38,7 +38,7 @@ function format_event_html($ev) { $o = '<div class="vevent">' . "\r\n"; - $o .= '<div class="event-title"><h3><i class="fa fa-calendar"></i> ' . zidify_links(smilies(bbcode($ev['summary']))) . '</h3></div>' . "\r\n"; + $o .= '<div class="event-title"><h3><i class="bi bi-calendar-date"></i> ' . zidify_links(smilies(bbcode($ev['summary']))) . '</h3></div>' . "\r\n"; $o .= '<div class="event-start"><span class="event-label">' . t('Starts:') . '</span> <span class="dtstart" title="' . datetime_convert('UTC', 'UTC', $ev['dtstart'], ((isset($ev['adjust']) && $ev['adjust']) ? ATOM_TIME : 'Y-m-d\TH:i:s' )) @@ -102,7 +102,15 @@ function format_event_obj($jobject) { if (is_array($object) && (array_key_exists('summary', $object) || array_key_exists('name', $object))) { $dtend = ((array_key_exists('endTime', $object)) ? $object['endTime'] : NULL_DATE); - $title = ((isset($object['summary']) && $object['summary']) ? zidify_links(smilies(bbcode($object['summary']))) : $object['name']); + + $title = $object['name'] ?? ''; + $content = html2bbcode($object['content']); + + if (isset($object['source']['content']) && strpos($object['source']['content'], '[/event-description]') !== false) { + $bbdescription = []; + preg_match("/\[event\-description\](.*?)\[\/event\-description\]/ism", $object['source']['content'], $bbdescription); + $content = $bbdescription[1]; + } // mobilizon sets a timezone in the object // we will assume that events with an timezone should be adjusted @@ -146,15 +154,8 @@ function format_event_obj($jobject) { '$event_tz' => ['label' => t('Timezone'), 'value' => (($tz === date_default_timezone_get()) ? '' : $tz)] )); - - $description = []; - - if (strpos($object['source']['content'], '[/event-description]') !== false) { - preg_match("/\[event\-description\](.*?)\[\/event\-description\]/ism", $object['source']['content'], $description); - } - $event['content'] = replace_macros(get_markup_template('event_item_content.tpl'), array( - '$description' => ((isset($description[1]))? zidify_links(smilies(bbcode($description[1]))) : EMPTY_STR), + '$description' => zidify_links(smilies(bbcode($content, ['tryoembed' => false]))), '$location_label' => t('Location:'), '$location' => ((array_path_exists('location/name', $object)) ? zidify_links(smilies(bbcode($object['location']['name']))) : EMPTY_STR) )); @@ -235,7 +236,7 @@ function ical_wrapper($ev) { if(! ((is_array($ev)) && count($ev))) return ''; - $o .= "BEGIN:VCALENDAR"; + $o = "BEGIN:VCALENDAR"; $o .= "\r\nVERSION:2.0"; $o .= "\r\nMETHOD:PUBLISH"; $o .= "\r\nPRODID:-//" . Config::Get('system','sitename') . "//" . Zotlabs\Lib\System::get_platform_name() . "//" . strtoupper(App::$language). "\r\n"; @@ -647,7 +648,7 @@ function event_store_event($arr) { else { try { $hash = Uuid::uuid4()->toString(); - } catch (UnsatisfiedDependencyException $e) { + } catch (UnableToBuildUuidException $e) { $hash = random_string(48); } } @@ -1070,7 +1071,7 @@ function event_import_ical($ical, $uid) { logger('storing event: ' . print_r($ev,true), LOGGER_ALL); $event = event_store_event($ev); if($event) { - $item_id = event_store_item($ev,$event); + event_store_item($ev, $event, false); return true; } } @@ -1205,7 +1206,7 @@ function event_import_ical_task($ical, $uid) { logger('storing event: ' . print_r($ev,true), LOGGER_ALL); $event = event_store_event($ev); if($event) { - $item_id = event_store_item($ev,$event); + event_store_item($ev, $event, false); return true; } } @@ -1214,8 +1215,7 @@ function event_import_ical_task($ical, $uid) { } - -function event_store_item($arr, $event) { +function event_store_item($arr, $event, $deliver = true) { require_once('include/datetime.php'); require_once('include/items.php'); @@ -1234,7 +1234,7 @@ function event_store_item($arr, $event) { } - $item_arr = array(); + $item_arr = []; $prefix = ''; // $birthday = false; @@ -1255,7 +1255,7 @@ function event_store_item($arr, $event) { $item_arr['comment_policy'] = 'none'; } - $r = q("SELECT * FROM item WHERE resource_id = '%s' AND resource_type = 'event' and uid = %d LIMIT 1", + $r = q("SELECT * FROM item left join xchan on author_xchan = xchan_hash WHERE resource_id = '%s' AND resource_type = 'event' and uid = %d LIMIT 1", dbesc($event['event_hash']), intval($arr['uid']) ); @@ -1292,51 +1292,21 @@ function event_store_item($arr, $event) { $object = json_encode($x); - $private = (($arr['allow_cid'] || $arr['allow_gid'] || $arr['deny_cid'] || $arr['deny_gid']) ? 1 : 0); - - /** - * @FIXME can only update sig if we have the author's channel on this site - * Until fixed, set it to nothing so it won't give us signature errors. - */ - $sig = ''; - - q("UPDATE item SET title = '%s', body = '%s', obj = '%s', allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s', edited = '%s', sig = '%s', item_flags = %d, item_private = %d, obj_type = '%s' WHERE id = %d AND uid = %d", - dbesc($arr['summary']), - dbesc($prefix . format_event_bbcode($arr)), - dbesc($object), - dbesc($arr['allow_cid']), - dbesc($arr['allow_gid']), - dbesc($arr['deny_cid']), - dbesc($arr['deny_gid']), - dbesc($arr['edited']), - dbesc($sig), - intval($r[0]['item_flags']), - intval($private), - dbesc('Event'), - intval($r[0]['id']), - intval($arr['uid']) - ); - - q("delete from term where oid = %d and otype = %d", - intval($r[0]['id']), - intval(TERM_OBJ_POST) - ); - - if(($arr['term']) && (is_array($arr['term']))) { - foreach($arr['term'] as $t) { - q("insert into term (uid,oid,otype,ttype,term,url) - values(%d,%d,%d,%d,'%s','%s') ", - intval($arr['uid']), - intval($r[0]['id']), - intval(TERM_OBJ_POST), - intval($t['ttype']), - dbesc($t['term']), - dbesc($t['url']) - ); - } - } - - $item_id = $r[0]['id']; + $item_arr['id'] = $r[0]['id']; + $item_arr['uid'] = $arr['uid']; + $item_arr['obj'] = $object; + $item_arr['edited'] = $arr['edited']; + $item_arr['allow_cid'] = $arr['allow_cid']; + $item_arr['allow_gid'] = $arr['allow_gid']; + $item_arr['deny_cid'] = $arr['deny_cid']; + $item_arr['deny_gid'] = $arr['deny_gid']; + $item_arr['item_private'] = (($arr['allow_cid'] || $arr['allow_gid'] || $arr['deny_cid'] || $arr['deny_gid']) ? 1 : 0); + $item_arr['title'] = $arr['summary']; + $item_arr['sig'] = ''; + $item_arr['body'] = $prefix . format_event_bbcode($arr); + $item_arr['term'] = $arr['term']; + + $post = item_store_update($item_arr, deliver: $deliver); /** * @hooks event_updated @@ -1344,14 +1314,10 @@ function event_store_item($arr, $event) { */ call_hooks('event_updated', $event['id']); - return $item_id; + return $post; } else { - $z = q("select * from channel where channel_id = %d limit 1", - intval($arr['uid']) - ); - - $private = (($arr['allow_cid'] || $arr['allow_gid'] || $arr['deny_cid'] || $arr['deny_gid']) ? 1 : 0); + $z = channelx_by_n($arr['uid']); $item_wall = 0; $item_origin = 0; @@ -1361,7 +1327,7 @@ function event_store_item($arr, $event) { $item_arr['id'] = $item['id']; } else { - $wall = (($z[0]['channel_hash'] == $event['event_xchan']) ? true : false); + $wall = (($z['channel_hash'] == $event['event_xchan']) ? true : false); $item_thread_top = 1; if($wall) { $item_wall = 1; @@ -1374,20 +1340,20 @@ function event_store_item($arr, $event) { $arr['mid'] = z_root() . '/activity/' . $event['event_hash']; } - $item_arr['aid'] = $z[0]['channel_account_id']; + $item_arr['aid'] = $z['channel_account_id']; $item_arr['uid'] = $arr['uid']; $item_arr['uuid'] = $arr['uuid']; $item_arr['author_xchan'] = $arr['event_xchan']; $item_arr['mid'] = $arr['mid']; $item_arr['parent_mid'] = $arr['mid']; - $item_arr['owner_xchan'] = (($wall) ? $z[0]['channel_hash'] : $arr['event_xchan']); + $item_arr['owner_xchan'] = (($wall) ? $z['channel_hash'] : $arr['event_xchan']); $item_arr['author_xchan'] = $arr['event_xchan']; $item_arr['title'] = $arr['summary']; $item_arr['allow_cid'] = $arr['allow_cid']; $item_arr['allow_gid'] = $arr['allow_gid']; $item_arr['deny_cid'] = $arr['deny_cid']; $item_arr['deny_gid'] = $arr['deny_gid']; - $item_arr['item_private'] = $private; + $item_arr['item_private'] = (($arr['allow_cid'] || $arr['allow_gid'] || $arr['deny_cid'] || $arr['deny_gid']) ? 1 : 0); $item_arr['verb'] = 'Invite'; $item_arr['item_wall'] = $item_wall; $item_arr['item_origin'] = $item_origin; @@ -1452,16 +1418,21 @@ function event_store_item($arr, $event) { $item_arr['obj'] = json_encode($y); } + $item_arr['target'] = [ + 'id' => str_replace('/item/', '/conversation/', $item_arr['parent_mid']), + 'type' => 'Collection', + 'attributedTo' => z_root() . '/channel/' . $z['channel_address'], + ]; + $item_arr['tgt_type'] = 'Collection'; + // propagate the event resource_id so that posts containing it are easily searchable in downstream copies // of the item which have not stored the actual event. Required for Diaspora event federation as Diaspora // event_participation messages refer to the event resource_id as a parent, while out own event attendance // activities refer to the item message_id as the parent. - set_iconfig($item_arr, 'system','event_id',$event['event_hash'],true); - - $res = item_store($item_arr); + set_iconfig($item_arr, 'system', 'event_id', $event['event_hash'], true); - $item_id = $res['item_id']; + $post = item_store($item_arr, deliver: $deliver); /** * @hooks event_created @@ -1469,7 +1440,7 @@ function event_store_item($arr, $event) { */ call_hooks('event_created', $event['id']); - return $item_id; + return $post; } } diff --git a/include/features.php b/include/features.php index d527f60e6..65ead6604 100644 --- a/include/features.php +++ b/include/features.php @@ -170,12 +170,20 @@ function get_features($filtered = true, $level = (-1)) { [ 'star_posts', t('Star Posts'), - t('Ability to mark special posts with a star indicator'), + t('Ability to mark conversations with a star'), false, Config::Get('feature_lock','star_posts'), ], [ + 'filing', + t('File Posts'), + t('Ability to file posts'), + false, + Config::Get('feature_lock','filing'), + ], + + [ 'reply_to', t('Reply on comment'), t('Ability to reply on selected comment'), diff --git a/include/feedutils.php b/include/feedutils.php index a50214746..1487ea2d6 100644 --- a/include/feedutils.php +++ b/include/feedutils.php @@ -228,15 +228,24 @@ function construct_activity_target($item) { if($item['target']) { $o = '<as:target>' . "\r\n"; $r = json_decode($item['target'],false); - if(! $r) + + if (!$r) { return ''; - if($r->type) + } + + if (isset($r->type)) { $o .= '<as:obj_type>' . xmlify($r->type) . '</as:obj_type>' . "\r\n"; - if($r->id) + } + + if (isset($r->id)) { $o .= '<id>' . xmlify($r->id) . '</id>' . "\r\n"; - if($r->title) + } + + if (isset($r->title)) { $o .= '<title>' . xmlify($r->title) . '</title>' . "\r\n"; - if($r->links) { + } + + if (isset($r->link)) { /** @FIXME !!! */ if(substr($r->link,0,1) === '<') { if(strstr($r->link,'&') && (! strstr($r->link,'&'))) @@ -244,11 +253,14 @@ function construct_activity_target($item) { $r->link = preg_replace('/\<link(.*?)\"\>/','<link$1"/>',$r->link); $o .= $r->link; } - else + else { $o .= '<link rel="alternate" type="text/html" href="' . xmlify($r->link) . '" />' . "\r\n"; + } } - if($r->content) + + if(isset($r->content)) { $o .= '<content type="html" >' . xmlify(bbcode($r->content)) . '</content>' . "\r\n"; + } $o .= '</as:target>' . "\r\n"; @@ -311,7 +323,7 @@ function get_atom_author($feed, $item) { $base = $rawactor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']; if($base && count($base)) { foreach($base as $link) { - if($link['attribs']['']['rel'] === 'alternate' && (! $res['author_link'])) + if($link['attribs']['']['rel'] === 'alternate' && (!$author['author_link'])) $author['author_link'] = unxmlify($link['attribs']['']['href']); if(!x($author, 'author_photo') || ! $author['author_photo']) { if($link['attribs']['']['rel'] === 'avatar' || $link['attribs']['']['rel'] === 'photo') @@ -357,7 +369,7 @@ function get_atom_author($feed, $item) { if($base && count($base)) { foreach($base as $link) { - if($link['attribs']['']['rel'] === 'alternate' && (! $res['author_link'])) + if($link['attribs']['']['rel'] === 'alternate' && (!$author['author_link'])) $author['author_link'] = unxmlify($link['attribs']['']['href']); if(! (x($author,'author_photo'))) { if($link['attribs']['']['rel'] === 'avatar' || $link['attribs']['']['rel'] === 'photo') @@ -998,7 +1010,7 @@ function process_feed_tombstones($feed,$importer,$contact,$pass) { if(! intval($item['item_deleted'])) { logger('deleting item ' . $item['id'] . ' mid=' . $item['mid'], LOGGER_DEBUG); - drop_item($item['id'],false); + drop_item($item['id']); } } } @@ -1173,15 +1185,19 @@ function consume_feed($xml, $importer, &$contact, $pass = 0) { intval($importer['channel_id']) ); - // Update content if 'updated' changes - if($r) { - if(activity_match($datarray['verb'], ['Delete', ACTIVITY_DELETE]) - && $datarray['author_xchan'] === $r[0]['author_xchan']) { + if ($r) { + // Check ownership + if ($datarray['author_xchan'] !== $r[0]['author_xchan']) { + logger('stored item author is not imported item author', LOGGER_DEBUG); + continue; + } + + if (activity_match($datarray['verb'], ['Delete', ACTIVITY_DELETE])) { if(! intval($r[0]['item_deleted'])) { logger('deleting item ' . $r[0]['id'] . ' mid=' . $datarray['mid'], LOGGER_DEBUG); - drop_item($r[0]['id'],false); + drop_item($r[0]['id']); } continue; } @@ -1444,12 +1460,17 @@ function consume_feed($xml, $importer, &$contact, $pass = 0) { // Update content if 'updated' changes - if($r) { - if(isset($datarray['verb']) && activity_match($datarray['verb'], ['Delete', ACTIVITY_DELETE]) - && isset($datarray['author_xchan']) && $datarray['author_xchan'] === $r[0]['author_xchan']) { + if ($r) { + // Check ownership + if ($datarray['author_xchan'] !== $r[0]['author_xchan']) { + logger('stored item author is not imported item author', LOGGER_DEBUG); + continue; + } + + if (isset($datarray['verb']) && activity_match($datarray['verb'], ['Delete', ACTIVITY_DELETE])) { if(! intval($r[0]['item_deleted'])) { logger('deleting item ' . $r[0]['id'] . ' mid=' . $datarray['mid'], LOGGER_DEBUG); - drop_item($r[0]['id'],false); + drop_item($r[0]['id'], uid: $importer['channel_id']); } continue; } diff --git a/include/help.php b/include/help.php index 12721a30b..2358f1289 100644 --- a/include/help.php +++ b/include/help.php @@ -1,6 +1,6 @@ <?php -use \Michelf\MarkdownExtra; +use Michelf\MarkdownExtra; use CommerceGuys\Intl\Language\LanguageRepository; require_once('include/items.php'); @@ -156,12 +156,12 @@ function store_doc_file($s) { if($r) { $item['id'] = $r[0]['id']; $item['mid'] = $item['parent_mid'] = $r[0]['mid']; - $x = item_store_update($item); + $x = item_store_update($item, deliver: false, addAndSync: false); } else { $item['uuid'] = item_message_id(); $item['mid'] = $item['parent_mid'] = z_root() . '/item/' . $item['uuid']; - $x = item_store($item); + $x = item_store($item, deliver: false, addAndSync: false); } return $x; diff --git a/include/html2bbcode.php b/include/html2bbcode.php index 8c35cdf03..ba298aa58 100644 --- a/include/html2bbcode.php +++ b/include/html2bbcode.php @@ -29,9 +29,6 @@ function node2bbcodesub(&$doc, $oldnode, $attributes, $startbb, $endbb) foreach ($list as $oldNode) { - if ($oldnode == 'li') - hz_syslog(print_r($oldNode,true)); - $attr = array(); if ($oldNode->attributes->length) foreach ($oldNode->attributes as $attribute) @@ -179,7 +176,7 @@ function html2bbcode($message) //node2bbcode($doc, 'span', array('style'=>'/.*font-size:\s*(\d+)[,;].*/'), '[size=$1]', '[/size]'); //node2bbcode($doc, 'span', array('style'=>'/.*font-size:\s*(.+?)[,;].*/'), '[size=$1]', '[/size]'); - node2bbcode($doc, 'span', array('style'=>'/.*color:\s*(.+?)[,;].*/'), '[color="$1"]', '[/color]'); + node2bbcode($doc, 'span', array('style'=>'/.*color:\s*(.+?)[,;].*/'), '[color=$1]', '[/color]'); //node2bbcode($doc, 'span', array('style'=>'/.*font-family:\s*(.+?)[,;].*/'), '[font=$1]', '[/font]'); //node2bbcode($doc, 'div', array('style'=>'/.*font-family:\s*(.+?)[,;].*font-size:\s*(\d+?)pt.*/'), '[font=$1][size=$2]', '[/size][/font]'); @@ -217,14 +214,11 @@ function html2bbcode($message) node2bbcode($doc, 'hr', array(), "[hr]", ""); -// node2bbcode($doc, 'table', array(), "", ""); -// node2bbcode($doc, 'tr', array(), "\n", ""); -// node2bbcode($doc, 'td', array(), "\t", ""); - - node2bbcode($doc, 'table', array(), "[table]", "[/table]"); node2bbcode($doc, 'th', array(), "[th]", "[/th]"); node2bbcode($doc, 'tr', array(), "[tr]", "[/tr]"); node2bbcode($doc, 'td', array(), "[td]", "[/td]"); + node2bbcode($doc, 'table', array(), "[table]", "[/table]"); + node2bbcode($doc, 'h1', array(), "[h1]", "[/h1]"); node2bbcode($doc, 'h2', array(), "[h2]", "[/h2]"); diff --git a/include/html2plain.php b/include/html2plain.php index 69fb5193a..6d580becf 100644 --- a/include/html2plain.php +++ b/include/html2plain.php @@ -120,6 +120,10 @@ function collecturls($message) { function html2plain($html, $wraplength = 75, $compact = false) { + if (!$html) { + return ''; + } + $message = str_replace("\r", "", $html); // mb_convert_encoding() is deprecated diff --git a/include/import.php b/include/import.php index 479c2c255..77d35a3e7 100644 --- a/include/import.php +++ b/include/import.php @@ -825,13 +825,13 @@ function import_items($channel, $items, $sync = false, $relocate = null) { if($item['edited'] >= $r[0]['edited']) { $item['id'] = $r[0]['id']; $item['uid'] = $channel['channel_id']; - $item_result = item_store_update($item,$allow_code,$deliver); + $item_result = item_store_update($item, $allow_code, $deliver, addAndSync: false); } } else { $item['aid'] = $channel['channel_account_id']; $item['uid'] = $channel['channel_id']; - $item_result = item_store($item,$allow_code,$deliver); + $item_result = item_store($item, $allow_code, $deliver, addAndSync: false); } // preserve conversations you've been involved in from being expired @@ -1886,7 +1886,7 @@ function import_webpage_element($element, $channel, $type) { $arr['id'] = $i[0]['id']; // don't update if it has the same timestamp as the original if($arr['edited'] > $i[0]['edited']) - $x = item_store_update($arr,$execflag); + $x = item_store_update($arr, $execflag, deliver: false, addAndSync: false); } else { if(($i) && (intval($i[0]['item_deleted']))) { @@ -1897,7 +1897,7 @@ function import_webpage_element($element, $channel, $type) { ); } else - $x = item_store($arr,$execflag); + $x = item_store($arr, $execflag, deliver: false, addAndSync: false); } if($x && $x['success']) { diff --git a/include/items.php b/include/items.php index dc9e9a7d9..b80c5672b 100644 --- a/include/items.php +++ b/include/items.php @@ -240,24 +240,46 @@ function comments_are_now_closed($item) { return false; } -function item_normal() { - $profile_uid = App::$profile['profile_uid'] ?? App::$profile_uid ?? null; +function item_normal($profile_uid = null, $prefix = 'item') { + if ($profile_uid === null) { + $profile_uid = App::$profile['profile_uid'] ?? App::$profile_uid ?? null; + } + $uid = local_channel(); $is_owner = ($uid && intval($profile_uid) === $uid); - $sql = " and item.item_hidden = 0 and item.item_type = 0 and item.item_deleted = 0 - and item.item_unpublished = 0 and item.item_pending_remove = 0"; + $sql = " and $prefix.item_hidden = 0 and $prefix.item_type = 0 and $prefix.item_deleted = 0 + and $prefix.item_unpublished = 0 and $prefix.item_pending_remove = 0"; if ($is_owner) { - $sql .= " and item.item_blocked IN (0, " . intval(ITEM_MODERATED) . ") and item.item_delayed IN (0, 1) "; + $sql .= " and $prefix.item_blocked IN (0, " . intval(ITEM_MODERATED) . ") and $prefix.item_delayed IN (0, 1) "; } else { - $sql .= " and item.item_blocked = 0 and item.item_delayed = 0 "; + $sql .= " and $prefix.item_blocked = 0 and $prefix.item_delayed = 0 "; } return $sql; } +function item_forwardable($item) { + if (intval($item['item_unpublished']) || + intval($item['item_delayed']) || + intval($item['item_blocked']) || + intval($item['item_hidden']) || + intval($item['item_restrict']) || // this might change in the future + // internal follow/unfollow thread + in_array($item['verb'], ['Follow', 'Ignore', ACTIVITY_FOLLOW, ACTIVITY_UNFOLLOW]) || + str_contains($item['postopts'], 'nodeliver') || + // actor not fetchable + (isset($item['author']['xchan_network']) && in_array($item['author']['xchan_network'], ['rss', 'anon', 'token'])) + + ) { + return false; + } + + return true; +} + function item_normal_search() { return " and item.item_hidden = 0 and item.item_type in (0,3,6,7) and item.item_deleted = 0 and item.item_unpublished = 0 and item.item_delayed = 0 and item.item_pending_remove = 0 @@ -361,7 +383,7 @@ function can_comment_on_post($observer_xchan, $item) { case 'specific': case 'contacts': case '': - if(local_channel() && get_abconfig(local_channel(), (($item['verb'] === ACTIVITY_SHARE) ? $item['source_xchan'] : $item['owner_xchan']), 'their_perms', 'post_comments')) { + if(local_channel() && get_abconfig(local_channel(), $item['owner_xchan'], 'their_perms', 'post_comments')) { return true; } if(intval($item['item_wall']) && perm_is_allowed($item['uid'],$observer_xchan,'post_comments')) { @@ -431,12 +453,13 @@ function add_source_route($iid, $hash) { * * \e boolean \b success true or false * * \e array \b activity the resulting activity if successful */ -function post_activity_item($arr, $allow_code = false, $deliver = true) { + +function post_activity_item($arr, $allow_code = false, $deliver = true, $channel = null, $observer = null, $addAndSync = true) { $ret = array('success' => false); $is_comment = false; - if((($arr['parent']) && $arr['parent'] != $arr['id']) || (($arr['parent_mid']) && $arr['parent_mid'] != $arr['mid'])) + if((isset($arr['parent'], $arr['id']) && intval($arr['parent']) !== intval($arr['id'])) || (isset($arr['parent_mid'], $arr['mid']) && $arr['parent_mid'] !== $arr['mid'])) $is_comment = true; if(! array_key_exists('item_origin',$arr)) @@ -446,11 +469,16 @@ function post_activity_item($arr, $allow_code = false, $deliver = true) { if(! array_key_exists('item_thread_top',$arr) && (! $is_comment)) $arr['item_thread_top'] = 1; - $channel = App::get_channel(); - $observer = App::get_observer(); + if (!$channel) { + $channel = App::get_channel(); + } + + if (!$observer) { + $observer = App::get_observer(); + } - $arr['aid'] = ((x($arr,'aid')) ? $arr['aid'] : $channel['channel_account_id']); - $arr['uid'] = ((x($arr,'uid')) ? $arr['uid'] : $channel['channel_id']); + $arr['aid'] = ((!empty($arr['aid'])) ? $arr['aid'] : $channel['channel_account_id']); + $arr['uid'] = ((!empty($arr['uid'])) ? $arr['uid'] : $channel['channel_id']); if(! perm_is_allowed($arr['uid'],$observer['xchan_hash'],(($is_comment) ? 'post_comments' : 'post_wall'))) { $ret['message'] = t('Permission denied'); @@ -466,18 +494,18 @@ function post_activity_item($arr, $allow_code = false, $deliver = true) { $arr['mimetype'] = 'text/bbcode'; - if(! $arr['mid']) { - $arr['uuid'] = ((x($arr,'uuid')) ? $arr['uuid'] : new_uuid()); + if(empty($arr['mid'])) { + $arr['uuid'] = ((!empty($arr['uuid'])) ? $arr['uuid'] : new_uuid()); } - $arr['mid'] = ((x($arr,'mid')) ? $arr['mid'] : z_root() . '/item/' . $arr['uuid']); - $arr['parent_mid'] = ((x($arr,'parent_mid')) ? $arr['parent_mid'] : $arr['mid']); - $arr['thr_parent'] = ((x($arr,'thr_parent')) ? $arr['thr_parent'] : $arr['mid']); + $arr['mid'] = ((!empty($arr['mid'])) ? $arr['mid'] : z_root() . '/item/' . $arr['uuid']); + $arr['parent_mid'] = ((!empty($arr['parent_mid'])) ? $arr['parent_mid'] : $arr['mid']); + $arr['thr_parent'] = ((!empty($arr['thr_parent'])) ? $arr['thr_parent'] : $arr['mid']); - $arr['owner_xchan'] = ((x($arr,'owner_xchan')) ? $arr['owner_xchan'] : $channel['channel_hash']); - $arr['author_xchan'] = ((x($arr,'author_xchan')) ? $arr['author_xchan'] : $observer['xchan_hash']); + $arr['owner_xchan'] = ((!empty($arr['owner_xchan'])) ? $arr['owner_xchan'] : $channel['channel_hash']); + $arr['author_xchan'] = ((!empty($arr['author_xchan'])) ? $arr['author_xchan'] : $observer['xchan_hash']); - $arr['verb'] = ((x($arr,'verb')) ? $arr['verb'] : 'Create'); - $arr['obj_type'] = ((x($arr,'obj_type')) ? $arr['obj_type'] : 'Note'); + $arr['verb'] = ((!empty($arr['verb'])) ? $arr['verb'] : 'Create'); + $arr['obj_type'] = ((!empty($arr['obj_type'])) ? $arr['obj_type'] : 'Note'); if(! ( array_key_exists('allow_cid',$arr) || array_key_exists('allow_gid',$arr) || array_key_exists('deny_cid',$arr) || array_key_exists('deny_gid',$arr))) { @@ -489,10 +517,18 @@ function post_activity_item($arr, $allow_code = false, $deliver = true) { $arr['comment_policy'] = map_scope(PermissionLimits::Get($channel['channel_id'],'post_comments')); - if ((! $arr['plink']) && (intval($arr['item_thread_top']))) { + if (empty($arr['plink']) && (intval($arr['item_thread_top']))) { $arr['plink'] = $arr['mid']; } + if (!$arr['target']) { + $arr['target'] = [ + 'id' => str_replace('/item/', '/conversation/', $arr['parent_mid']), + 'type' => 'Collection', + 'attributedTo' => z_root() . '/channel/' . $channel['channel_address'], + ]; + $arr['tgt_type'] = 'Collection'; + } // for the benefit of plugins, we will behave as if this is an API call rather than a normal online post @@ -504,34 +540,35 @@ function post_activity_item($arr, $allow_code = false, $deliver = true) { */ call_hooks('post_local', $arr); - if(x($arr, 'cancel')) { + if (!empty($arr['cancel'])) { logger('Post cancelled by plugin.'); return $ret; } - $post = item_store($arr,$allow_code,$deliver); - - if($post['success']) { - $post_id = $post['item_id']; - $ret['success'] = true; - $ret['item_id'] = $post_id; - $ret['activity'] = $post['item']; + $post = item_store($arr, $allow_code, $deliver, $addAndSync); - /** - * @hooks post_local_end - * Called after a local post operation has completed. - * * \e array - the item returned from item_store() - */ - call_hooks('post_local_end', $ret['activity']); - } - else + if (!$post['success']) { return $ret; - - if($post_id && $deliver) { - Master::Summon(['Notifier','activity', $post_id]); } + $post_id = $post['item_id']; $ret['success'] = true; + $ret['item_id'] = $post_id; + $ret['activity'] = $post['item']; + + /** + * @hooks post_local_end + * Called after a local post operation has completed. + * * \e array - the item returned from item_store() + */ + call_hooks('post_local_end', $ret['activity']); + + if($post_id && $deliver) { + Master::Summon(['Notifier', 'activity', $post_id]); + if (!empty($post['approval_id'])) { + Master::Summon(['Notifier', 'activity', $post['approval_id']]); + } + } return $ret; } @@ -693,14 +730,14 @@ function get_item_elements($x,$allow_code = false) { if($arr['edited'] > datetime_convert()) $arr['edited'] = datetime_convert(); - $arr['expires'] = ((x($x,'expires') && $x['expires']) + $arr['expires'] = ((!empty($x['expires']) && $x['expires']) ? datetime_convert('UTC','UTC',$x['expires']) : NULL_DATE); - $arr['commented'] = ((x($x,'commented') && $x['commented']) + $arr['commented'] = ((!empty($x['commented']) && $x['commented']) ? datetime_convert('UTC','UTC',$x['commented']) : $arr['created']); - $arr['comments_closed'] = ((x($x,'comments_closed') && $x['comments_closed']) + $arr['comments_closed'] = ((!empty($x['comments_closed']) && $x['comments_closed']) ? datetime_convert('UTC','UTC',$x['comments_closed']) : NULL_DATE); @@ -1594,7 +1631,7 @@ function item_json_encapsulate($arr, $k) { * * \e boolean \b success * * \e int \b item_id */ -function item_store($arr, $allow_exec = false, $deliver = true) { +function item_store($arr, $allow_exec = false, $deliver = true, $addAndSync = true) { $d = [ 'item' => $arr, @@ -1645,7 +1682,7 @@ function item_store($arr, $allow_exec = false, $deliver = true) { if(array_key_exists('parent',$arr)) unset($arr['parent']); - $arr['mimetype'] = ((x($arr,'mimetype')) ? notags(trim($arr['mimetype'])) : 'text/bbcode'); + $arr['mimetype'] = ((!empty($arr['mimetype'])) ? notags(trim($arr['mimetype'])) : 'text/bbcode'); if(($arr['mimetype'] == 'application/x-php') && (! $allow_exec)) { logger('item_store: php mimetype but allow_exec is denied.'); @@ -1657,19 +1694,19 @@ function item_store($arr, $allow_exec = false, $deliver = true) { $arr['summary'] = ((array_key_exists('summary',$arr) && $arr['summary']) ? trim($arr['summary']) : ''); $arr['body'] = ((array_key_exists('body',$arr) && $arr['body']) ? trim($arr['body']) : ''); - $arr['allow_cid'] = ((x($arr,'allow_cid')) ? trim($arr['allow_cid']) : ''); - $arr['allow_gid'] = ((x($arr,'allow_gid')) ? trim($arr['allow_gid']) : ''); - $arr['deny_cid'] = ((x($arr,'deny_cid')) ? trim($arr['deny_cid']) : ''); - $arr['deny_gid'] = ((x($arr,'deny_gid')) ? trim($arr['deny_gid']) : ''); - $arr['postopts'] = ((x($arr,'postopts')) ? trim($arr['postopts']) : ''); - $arr['route'] = ((x($arr,'route')) ? trim($arr['route']) : ''); - $arr['uuid'] = ((x($arr,'uuid')) ? trim($arr['uuid']) : ''); - $arr['item_private'] = ((x($arr,'item_private')) ? intval($arr['item_private']) : 0 ); - $arr['item_wall'] = ((x($arr,'item_wall')) ? intval($arr['item_wall']) : 0 ); - $arr['item_type'] = ((x($arr,'item_type')) ? intval($arr['item_type']) : 0 ); + $arr['allow_cid'] = ((!empty($arr['allow_cid'])) ? trim($arr['allow_cid']) : ''); + $arr['allow_gid'] = ((!empty($arr['allow_gid'])) ? trim($arr['allow_gid']) : ''); + $arr['deny_cid'] = ((!empty($arr['deny_cid'])) ? trim($arr['deny_cid']) : ''); + $arr['deny_gid'] = ((!empty($arr['deny_gid'])) ? trim($arr['deny_gid']) : ''); + $arr['postopts'] = ((!empty($arr['postopts'])) ? trim($arr['postopts']) : ''); + $arr['route'] = ((!empty($arr['route'])) ? trim($arr['route']) : ''); + $arr['uuid'] = ((!empty($arr['uuid'])) ? trim($arr['uuid']) : ''); + $arr['item_private'] = ((!empty($arr['item_private'])) ? intval($arr['item_private']) : 0 ); + $arr['item_wall'] = ((!empty($arr['item_wall'])) ? intval($arr['item_wall']) : 0 ); + $arr['item_type'] = ((!empty($arr['item_type'])) ? intval($arr['item_type']) : 0 ); // obsolete, but needed so as not to throw not-null constraints on some database driveres - $arr['item_flags'] = ((x($arr,'item_flags')) ? intval($arr['item_flags']) : 0 ); + $arr['item_flags'] = ((!empty($arr['item_flags'])) ? intval($arr['item_flags']) : 0 ); $arr['lang'] = detect_language($arr['body']); @@ -1710,32 +1747,32 @@ function item_store($arr, $allow_exec = false, $deliver = true) { $arr = $translate['item']; } - if((x($arr,'obj')) && is_array($arr['obj'])) { + if((!empty($arr['obj'])) && is_array($arr['obj'])) { activity_sanitise($arr['obj']); $arr['obj'] = json_encode($arr['obj']); } - if((x($arr,'target')) && is_array($arr['target'])) { + if((!empty($arr['target'])) && is_array($arr['target'])) { activity_sanitise($arr['target']); $arr['target'] = json_encode($arr['target']); } - if((x($arr,'attach')) && is_array($arr['attach'])) { + if((!empty($arr['attach'])) && is_array($arr['attach'])) { activity_sanitise($arr['attach']); $arr['attach'] = json_encode($arr['attach']); } - $arr['aid'] = ((x($arr,'aid')) ? intval($arr['aid']) : 0); - $arr['mid'] = ((x($arr,'mid')) ? notags(trim($arr['mid'])) : random_string()); - $arr['revision'] = ((x($arr,'revision') && intval($arr['revision']) > 0) ? intval($arr['revision']) : 0); + $arr['aid'] = ((!empty($arr['aid'])) ? intval($arr['aid']) : 0); + $arr['mid'] = ((!empty($arr['mid'])) ? notags(trim($arr['mid'])) : random_string()); + $arr['revision'] = ((!empty($arr['revision']) && intval($arr['revision']) > 0) ? intval($arr['revision']) : 0); - $arr['author_xchan'] = ((x($arr,'author_xchan')) ? notags(trim($arr['author_xchan'])) : ''); - $arr['owner_xchan'] = ((x($arr,'owner_xchan')) ? notags(trim($arr['owner_xchan'])) : ''); - $arr['created'] = ((x($arr,'created') !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert()); - $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert()); - $arr['expires'] = ((x($arr,'expires') !== false) ? datetime_convert('UTC','UTC',$arr['expires']) : NULL_DATE); - $arr['commented'] = ((x($arr,'commented') !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert()); - $arr['comments_closed'] = ((x($arr,'comments_closed') !== false) ? datetime_convert('UTC','UTC',$arr['comments_closed']) : NULL_DATE); + $arr['author_xchan'] = ((!empty($arr['author_xchan'])) ? notags(trim($arr['author_xchan'])) : ''); + $arr['owner_xchan'] = ((!empty($arr['owner_xchan'])) ? notags(trim($arr['owner_xchan'])) : ''); + $arr['created'] = ((!empty($arr['created']) !== false) ? datetime_convert('UTC','UTC',$arr['created']) : datetime_convert()); + $arr['edited'] = ((!empty($arr['edited']) !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert()); + $arr['expires'] = ((!empty($arr['expires']) !== false) ? datetime_convert('UTC','UTC',$arr['expires']) : NULL_DATE); + $arr['commented'] = ((!empty($arr['commented']) !== false) ? datetime_convert('UTC','UTC',$arr['commented']) : datetime_convert()); + $arr['comments_closed'] = ((!empty($arr['comments_closed']) !== false) ? datetime_convert('UTC','UTC',$arr['comments_closed']) : NULL_DATE); $arr['html'] = ((array_key_exists('html',$arr)) ? $arr['html'] : ''); if($deliver) { @@ -1749,26 +1786,26 @@ function item_store($arr, $allow_exec = false, $deliver = true) { // will still take place through backdoor methods. Since these fields are rarely used // otherwise, just preserve the original timestamp. - $arr['received'] = ((x($arr,'received') !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert()); - $arr['changed'] = ((x($arr,'changed') !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert()); + $arr['received'] = ((!empty($arr['received']) !== false) ? datetime_convert('UTC','UTC',$arr['received']) : datetime_convert()); + $arr['changed'] = ((!empty($arr['changed']) !== false) ? datetime_convert('UTC','UTC',$arr['changed']) : datetime_convert()); } - $arr['location'] = ((x($arr,'location')) ? notags(trim($arr['location'])) : ''); - $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : ''); - $arr['parent_mid'] = ((x($arr,'parent_mid')) ? notags(trim($arr['parent_mid'])) : ''); - $arr['thr_parent'] = ((x($arr,'thr_parent')) ? notags(trim($arr['thr_parent'])) : $arr['parent_mid']); - $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : 'Create'); - $arr['obj_type'] = ((x($arr,'obj_type')) ? notags(trim($arr['obj_type'])) : 'Note'); - $arr['obj'] = ((x($arr,'obj')) ? trim($arr['obj']) : ''); - $arr['tgt_type'] = ((x($arr,'tgt_type')) ? notags(trim($arr['tgt_type'])) : ''); - $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : ''); - $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : ''); - $arr['attach'] = ((x($arr,'attach')) ? notags(trim($arr['attach'])) : ''); - $arr['app'] = ((x($arr,'app')) ? notags(trim($arr['app'])) : ''); + $arr['location'] = ((!empty($arr['location'])) ? notags(trim($arr['location'])) : ''); + $arr['coord'] = ((!empty($arr['coord'])) ? notags(trim($arr['coord'])) : ''); + $arr['parent_mid'] = ((!empty($arr['parent_mid'])) ? notags(trim($arr['parent_mid'])) : ''); + $arr['thr_parent'] = ((!empty($arr['thr_parent'])) ? notags(trim($arr['thr_parent'])) : $arr['parent_mid']); + $arr['verb'] = ((!empty($arr['verb'])) ? notags(trim($arr['verb'])) : 'Create'); + $arr['obj_type'] = ((!empty($arr['obj_type'])) ? notags(trim($arr['obj_type'])) : 'Note'); + $arr['obj'] = ((!empty($arr['obj'])) ? trim($arr['obj']) : ''); + $arr['tgt_type'] = ((!empty($arr['tgt_type'])) ? notags(trim($arr['tgt_type'])) : ''); + $arr['target'] = ((!empty($arr['target'])) ? trim($arr['target']) : ''); + $arr['plink'] = ((!empty($arr['plink'])) ? notags(trim($arr['plink'])) : ''); + $arr['attach'] = ((!empty($arr['attach'])) ? notags(trim($arr['attach'])) : ''); + $arr['app'] = ((!empty($arr['app'])) ? notags(trim($arr['app'])) : ''); - $arr['public_policy'] = ((x($arr,'public_policy')) ? notags(trim($arr['public_policy'])) : '' ); + $arr['public_policy'] = ((!empty($arr['public_policy'])) ? notags(trim($arr['public_policy'])) : '' ); - $arr['comment_policy'] = ((x($arr,'comment_policy')) ? notags(trim($arr['comment_policy'])) : 'contacts' ); + $arr['comment_policy'] = ((!empty($arr['comment_policy'])) ? notags(trim($arr['comment_policy'])) : 'contacts' ); if(! array_key_exists('item_unseen',$arr)) $arr['item_unseen'] = 1; @@ -1776,16 +1813,6 @@ function item_store($arr, $allow_exec = false, $deliver = true) { if((! array_key_exists('item_nocomment',$arr)) && ($arr['comment_policy'] == 'none')) $arr['item_nocomment'] = 1; - // handle time travelers - // Allow a bit of fudge in case somebody just has a slightly slow/fast clock - - $d1 = new DateTime('now +10 minutes', new DateTimeZone('UTC')); - $d2 = new DateTime($arr['created'] . '+00:00'); - - if($d2 > $d1) { - $arr['item_delayed'] = 1; - } - if(empty($arr['llink'])) { $arr['llink'] = z_root() . '/display/' . $arr['uuid']; } @@ -1827,7 +1854,7 @@ function item_store($arr, $allow_exec = false, $deliver = true) { ); } - if(comments_are_now_closed($r[0])) { + if(comments_are_now_closed($r[0]) && !in_array($arr['verb'], ['Add', 'Remove'])) { logger('item_store: comments closed'); $ret['message'] = 'Comments closed.'; return $ret; @@ -1882,7 +1909,7 @@ function item_store($arr, $allow_exec = false, $deliver = true) { $arr['item_private'] = 0; if(in_array($arr['obj_type'], ['Note','Answer']) && $r[0]['obj_type'] === 'Question' && intval($r[0]['item_wall'])) { - Activity::update_poll($r[0]['id'], $arr); + Activity::update_poll($r[0], $arr); } } @@ -1922,7 +1949,7 @@ function item_store($arr, $allow_exec = false, $deliver = true) { */ call_hooks('post_remote', $arr); - if(x($arr, 'cancel')) { + if(!empty($arr['cancel'])) { logger('Post cancelled by plugin.'); $ret['message'] = 'cancelled.'; return $ret; @@ -1965,7 +1992,9 @@ function item_store($arr, $allow_exec = false, $deliver = true) { // find the item we just created - $r = q("SELECT * FROM item WHERE mid = '%s' AND uid = %d and revision = %d ORDER BY id ASC ", + $r = q("SELECT item.*, tp.uuid AS thr_parent_uuid FROM item + LEFT JOIN item tp ON item.thr_parent = tp.mid AND item.uid = tp.uid + WHERE item.mid = '%s' AND item.uid = %d and item.revision = %d ORDER BY item.id ASC ", dbesc($arr['mid']), intval($arr['uid']), intval($arr['revision']) @@ -2047,9 +2076,15 @@ function item_store($arr, $allow_exec = false, $deliver = true) { item_update_parent_commented($arr); + if (str_contains($arr['body'], '[/embed]') || str_contains($arr['body'], '[/img]') || str_contains($arr['body'], '[/zmg]')) { + Master::Summon(['Cache_embeds', $arr['uuid']]); + } - if(strpos($arr['body'],'[embed]') !== false) { - Master::Summon([ 'Cache_embeds', $current_post ]); + $ret['success'] = true; + $ret['item_id'] = $current_post; + + if ($addAndSync) { + $ret = addToCollectionAndSync($ret); } // If _creating_ a deleted item, don't propagate it further or send out notifications. @@ -2062,9 +2097,6 @@ function item_store($arr, $allow_exec = false, $deliver = true) { tag_deliver($arr['uid'],$current_post); } - $ret['success'] = true; - $ret['item_id'] = $current_post; - return $ret; } @@ -2077,7 +2109,7 @@ function item_store($arr, $allow_exec = false, $deliver = true) { * @param boolean $deliver (optional) default true * @return array */ -function item_store_update($arr, $allow_exec = false, $deliver = true) { +function item_store_update($arr, $allow_exec = false, $deliver = true, $addAndSync = true) { $d = [ 'item' => $arr, @@ -2135,7 +2167,7 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { if(array_key_exists('edit',$arr)) unset($arr['edit']); - $arr['mimetype'] = ((x($arr,'mimetype')) ? notags(trim($arr['mimetype'])) : 'text/bbcode'); + $arr['mimetype'] = ((!empty($arr['mimetype'])) ? notags(trim($arr['mimetype'])) : 'text/bbcode'); if(($arr['mimetype'] == 'application/x-php') && (! $allow_exec)) { logger('item_store: php mimetype but allow_exec is denied.'); @@ -2206,12 +2238,12 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { unset($arr['thr_parent']); unset($arr['llink']); - $arr['edited'] = ((x($arr,'edited') !== false) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert()); - $arr['expires'] = ((x($arr,'expires') !== false) ? datetime_convert('UTC','UTC',$arr['expires']) : $orig[0]['expires']); + $arr['edited'] = ((!empty($arr['edited'])) ? datetime_convert('UTC','UTC',$arr['edited']) : datetime_convert()); + $arr['expires'] = ((!empty($arr['expires'])) ? datetime_convert('UTC','UTC',$arr['expires']) : $orig[0]['expires']); - $arr['revision'] = ((x($arr,'revision') && $arr['revision'] > 0) ? intval($arr['revision']) : 0); + $arr['revision'] = ((!empty($arr['revision'])) ? intval($arr['revision']) : 0); - if(array_key_exists('comments_closed',$arr) && $arr['comments_closed'] > NULL_DATE) + if(array_key_exists('comments_closed',$arr)) $arr['comments_closed'] = datetime_convert('UTC','UTC',$arr['comments_closed']); else $arr['comments_closed'] = $orig[0]['comments_closed']; @@ -2223,15 +2255,15 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { $arr['route'] = ((array_key_exists('route',$arr)) ? trim($arr['route']) : $orig[0]['route']); - $arr['location'] = ((x($arr,'location')) ? notags(trim($arr['location'])) : $orig[0]['location']); - $arr['uuid'] = ((x($arr,'uuid')) ? notags(trim($arr['uuid'])) : $orig[0]['uuid']); - $arr['coord'] = ((x($arr,'coord')) ? notags(trim($arr['coord'])) : $orig[0]['coord']); - $arr['verb'] = ((x($arr,'verb')) ? notags(trim($arr['verb'])) : $orig[0]['verb']); - $arr['obj_type'] = ((x($arr,'obj_type')) ? notags(trim($arr['obj_type'])) : $orig[0]['obj_type']); - $arr['obj'] = ((x($arr,'obj')) ? trim($arr['obj']) : $orig[0]['obj']); - $arr['tgt_type'] = ((x($arr,'tgt_type')) ? notags(trim($arr['tgt_type'])) : $orig[0]['tgt_type']); - $arr['target'] = ((x($arr,'target')) ? trim($arr['target']) : $orig[0]['target']); - $arr['plink'] = ((x($arr,'plink')) ? notags(trim($arr['plink'])) : $orig[0]['plink']); + $arr['location'] = ((!empty($arr['location'])) ? notags(trim($arr['location'])) : $orig[0]['location']); + $arr['uuid'] = ((!empty($arr['uuid'])) ? notags(trim($arr['uuid'])) : $orig[0]['uuid']); + $arr['coord'] = ((!empty($arr['coord'])) ? notags(trim($arr['coord'])) : $orig[0]['coord']); + $arr['verb'] = ((!empty($arr['verb'])) ? notags(trim($arr['verb'])) : $orig[0]['verb']); + $arr['obj_type'] = ((!empty($arr['obj_type'])) ? notags(trim($arr['obj_type'])) : $orig[0]['obj_type']); + $arr['obj'] = ((!empty($arr['obj'])) ? trim($arr['obj']) : $orig[0]['obj']); + $arr['tgt_type'] = ((!empty($arr['tgt_type'])) ? notags(trim($arr['tgt_type'])) : $orig[0]['tgt_type']); + $arr['target'] = ((!empty($arr['target'])) ? trim($arr['target']) : $orig[0]['target']); + $arr['plink'] = ((!empty($arr['plink'])) ? notags(trim($arr['plink'])) : $orig[0]['plink']); $arr['allow_cid'] = ((array_key_exists('allow_cid',$arr)) ? trim($arr['allow_cid']) : $orig[0]['allow_cid']); $arr['allow_gid'] = ((array_key_exists('allow_gid',$arr)) ? trim($arr['allow_gid']) : $orig[0]['allow_gid']); @@ -2270,11 +2302,11 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { $arr['item_pending_remove'] = ((array_key_exists('item_pending_remove',$arr)) ? intval($arr['item_pending_remove']) : $orig[0]['item_pending_remove'] ); $arr['item_blocked'] = ((array_key_exists('item_blocked',$arr)) ? intval($arr['item_blocked']) : $orig[0]['item_blocked'] ); - $arr['sig'] = ((x($arr,'sig')) ? $arr['sig'] : ''); + $arr['sig'] = ((!empty($arr['sig'])) ? $arr['sig'] : ''); $arr['layout_mid'] = ((array_key_exists('layout_mid',$arr)) ? dbesc($arr['layout_mid']) : $orig[0]['layout_mid'] ); - $arr['public_policy'] = ((x($arr,'public_policy')) ? notags(trim($arr['public_policy'])) : $orig[0]['public_policy'] ); - $arr['comment_policy'] = ((x($arr,'comment_policy')) ? notags(trim($arr['comment_policy'])) : $orig[0]['comment_policy'] ); + $arr['public_policy'] = ((!empty($arr['public_policy'])) ? notags(trim($arr['public_policy'])) : $orig[0]['public_policy'] ); + $arr['comment_policy'] = ((!empty($arr['comment_policy'])) ? notags(trim($arr['comment_policy'])) : $orig[0]['comment_policy'] ); /** * @hooks post_remote_update @@ -2282,7 +2314,7 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { */ call_hooks('post_remote_update', $arr); - if(x($arr, 'cancel')) { + if(!empty($arr['cancel'])) { logger('Post cancelled by plugin.'); $ret['message'] = 'cancelled.'; return $ret; @@ -2334,7 +2366,9 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { // fetch an unescaped complete copy of the stored item - $r = q("select * from item where id = %d", + $r = q("SELECT item.*, tp.uuid AS thr_parent_uuid FROM item + LEFT JOIN item tp ON item.thr_parent = tp.mid AND item.uid = tp.uid + WHERE item.id = %d", intval($orig_post_id) ); if($r) @@ -2348,14 +2382,15 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { if(is_array($terms)) { foreach($terms as $t) { - q("insert into term (uid,oid,otype,ttype,term,url) - values(%d,%d,%d,%d,'%s','%s') ", + q("insert into term (uid, oid, otype, ttype, term, url, imgurl) + values (%d, %d, %d, %d, '%s', '%s', '%s')", intval($uid), intval($orig_post_id), intval(TERM_OBJ_POST), intval($t['ttype']), dbesc($t['term']), - dbesc($t['url']) + dbesc($t['url']), + dbesc($t['imgurl'] ?? ''), ); } $arr['term'] = $terms; @@ -2386,21 +2421,23 @@ function item_store_update($arr, $allow_exec = false, $deliver = true) { */ call_hooks('item_stored_update',$arr); - if(strpos($arr['body'],'[embed]') !== false) { - Master::Summon([ 'Cache_embeds', $orig_post_id ]); + if (str_contains($arr['body'], '[/embed]') || str_contains($arr['body'], '[/img]') || str_contains($arr['body'], '[/zmg]')) { + Master::Summon(['Cache_embeds', $arr['uuid']]); } + $ret['success'] = true; + $ret['item_id'] = $orig_post_id; - + if ($addAndSync) { + $ret = addToCollectionAndSync($ret); + } if($deliver) { - send_status_notifications($orig_post_id,$arr); + // don't send notify_comment for edits + // send_status_notifications($orig_post_id,$arr); tag_deliver($uid,$orig_post_id); } - $ret['success'] = true; - $ret['item_id'] = $orig_post_id; - return $ret; } @@ -2448,21 +2485,23 @@ function send_status_notifications($post_id,$item) { $parent = 0; $is_reaction = false; - $thr_parent_id = 0; + $thr_parent_id = null; + $thr_parent_uuid = null; $type = ((intval($item['item_private']) === 2) ? NOTIFY_MAIL : NOTIFY_COMMENT); - if(array_key_exists('verb',$item) && activity_match($item['verb'], ['Like', 'Dislike', ACTIVITY_LIKE, ACTIVITY_DISLIKE])) { + if(array_key_exists('verb',$item) && activity_match($item['verb'], ['Like', 'Dislike', ACTIVITY_LIKE, ACTIVITY_DISLIKE, 'Announce'])) { $type = NOTIFY_LIKE; - $r = q("select id from item where mid = '%s' and uid = %d limit 1", + $r = q("select id, uuid from item where mid = '%s' and uid = %d limit 1", dbesc($item['thr_parent']), intval($item['uid']) ); if ($r) { $thr_parent_id = $r[0]['id']; + $thr_parent_uuid = $r[0]['uuid']; } } @@ -2487,6 +2526,7 @@ function send_status_notifications($post_id,$item) { dbesc($item['parent_mid']), intval($item['uid']) ); + if($x) { foreach($x as $xx) { if($xx['author_xchan'] === $r[0]['channel_hash']) { @@ -2531,7 +2571,7 @@ function send_status_notifications($post_id,$item) { 'link' => $link, 'verb' => $item['verb'], 'otype' => 'item', - 'parent' => $thr_parent_id ? $thr_parent_id : $parent, + 'parent' => $thr_parent_id ?? $parent, 'parent_mid' => $thr_parent_id ? $item['thr_parent'] : $item['parent_mid'] )); } @@ -2808,8 +2848,8 @@ function tag_deliver($uid, $item_id) { 'from_xchan' => $item['author_xchan'], 'type' => NOTIFY_TAGSELF, 'item' => $item, - 'link' => $i[0]['llink'], - 'verb' => ACTIVITY_TAG, + 'link' => $item['llink'], + 'verb' => $item['verb'], 'otype' => 'item' )); @@ -2951,35 +2991,32 @@ function tgroup_check($uid, $item) { // post to group via DM if ($is_group) { - if (intval($item['item_private']) === 2 && $item['mid'] === $item['parent_mid']) { + if (intval($item['item_private']) === 2 && $item['mid'] === $item['parent_mid'] && perm_is_allowed($uid, $item['owner_xchan'], 'post_wall')) { return true; } } - // see if we already have this item. Maybe it is being updated. $r = q("select id from item where mid = '%s' and uid = %d limit 1", dbesc($item['mid']), intval($uid) ); - if($r) + + if ($r) { return true; + } - if(! perm_is_allowed($uid,$item['author_xchan'],'tag_deliver')) - return false; + $u = channelx_by_n($uid); - $u = q("select * from channel left join xchan on channel_hash = xchan_hash where channel_id = %d limit 1", - intval($uid) - ); - - if(! $u) + if (!$u) { return false; + } $max_forums = Config::Get('system','max_tagged_forums',2); $matched_forums = 0; - $link = normalise_link($u[0]['xchan_url']); + $link = normalise_link($u['xchan_url']); $terms = []; @@ -2996,7 +3033,7 @@ function tgroup_check($uid, $item) { } $mention = true; - logger('tgroup_check: mention found for ' . $u[0]['channel_name']); + logger('tgroup_check: mention found for ' . $u['channel_name']); // At this point we've determined that the person receiving this post was mentioned in it. // Now let's check if this mention was inside a reshare so we don't spam a forum @@ -3104,6 +3141,11 @@ function i_am_mentioned($channel, $item, $check_groups = false) { */ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false, $edit = false) { + if ($item['author_xchan'] === $channel['channel_hash'] && in_array($item['verb'], ['Add', 'Remove'])) { + logger('delivery chain already started'); + return; + } + $sourced = check_item_source($channel['channel_id'],$item); if($sourced) { @@ -3149,17 +3191,113 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false $item['parent_mid'] = $item['mid']; $item['thr_parent'] = $item['mid']; $item['llink'] = z_root() . '/display/' . $item['uuid']; + $item['target'] = json_encode([ + 'id' => str_replace('/item/', '/conversation/', $item['mid']), + 'type' => 'Collection', + 'attributedTo' => z_root() . '/channel/' . $channel['channel_address'] + ]); + $item['tgt_type'] = 'Collection'; } + } - $r = q("UPDATE item SET author_xchan = '%s', mid = '%s', parent_mid = '%s', thr_parent = '%s', llink = '%s' WHERE id = %d", - dbesc($item['author_xchan']), - dbesc($item['mid']), - dbesc($item['parent_mid']), - dbesc($item['thr_parent']), - dbesc($item['llink']), + $private = (($channel['channel_allow_cid'] || $channel['channel_allow_gid'] + || $channel['channel_deny_cid'] || $channel['channel_deny_gid']) ? 1 : 0); + + $new_public_policy = map_scope(PermissionLimits::Get($channel['channel_id'],'view_stream'),true); + + if((! $private) && $new_public_policy) + $private = 1; + + $item_wall = 1; + $item_origin = (($item['item_deleted']) ? 0 : 1); // item_origin for deleted items is set to 0 in delete_imported_item() to prevent looping. In this case we probably should not set it back to 1 here. + $item_uplink = 0; + $item_nocomment = 0; + + $flag_bits = $item['item_flags']; + + // maintain the original source, which will be the original item owner and was stored in source_xchan + // when we created the delivery fork + + if($parent) { + $r = q("update item set source_xchan = '%s' where id = %d", + dbesc($parent['source_xchan']), intval($item_id) ); } + else { + $item_uplink = (($item['item_rss']) ? 0 : 1); // Do not set item_uplink for rss items - we can not send anything to them. + + // if this is an edit, item_store_update() will have already updated the item + // with the correct value for source_xchan (by ignoring it). We cannot set to owner_xchan + // in this case because owner_xchan will point to the parent of this chain + // and not the original sender. + + if(!$edit) { + $r = q("update item set source_xchan = owner_xchan where id = %d", + intval($item_id) + ); + } + } + + // this will not work with item_store_update() + + $r = q("update item set item_uplink = %d, item_nocomment = %d, item_flags = %d, owner_xchan = '%s', allow_cid = '%s', allow_gid = '%s', + deny_cid = '%s', deny_gid = '%s', item_private = %d, public_policy = '%s', comment_policy = '%s', title = '%s', body = '%s', item_wall = %d, item_origin = %d, + author_xchan = '%s', mid = '%s', parent_mid = '%s', thr_parent = '%s', llink = '%s', target = '%s', tgt_type = '%s' where id = %d", + intval($item_uplink), + intval($item_nocomment), + intval($flag_bits), + dbesc($channel['channel_hash']), + dbesc($channel['channel_allow_cid']), + dbesc($channel['channel_allow_gid']), + dbesc($channel['channel_deny_cid']), + dbesc($channel['channel_deny_gid']), + intval($private), + dbesc($new_public_policy), + dbesc(map_scope(PermissionLimits::Get($channel['channel_id'],'post_comments'))), + dbesc($item['title']), + dbesc($item['body']), + intval($item_wall), + intval($item_origin), + dbesc($item['author_xchan']), + dbesc($item['mid']), + dbesc($item['parent_mid']), + dbesc($item['thr_parent']), + dbesc($item['llink']), + dbesc($item['target']), + dbesc($item['tgt_type']), + intval($item_id) + ); + + if($r) { + $rr = q("select * from item where id = %d", + intval($item_id) + ); + + if ($rr) { + + // this is hackish but since we can not use item_store_update() here, + // we will prepare a similar output to feed to addToCollectionAndSync() + $ret['success'] = 1; + $ret['item_id'] = $rr[0]['id']; + $ret['item'] = $rr[0]; + + $result = addToCollectionAndSync($ret); + + Master::Summon(['Notifier', 'tgroup', $result['item_id']]); + if ($result['approval_id']) { + Master::Summon(['Notifier', 'tgroup', $result['approval_id']]); + } + } + } + else { + logger('start_delivery_chain: failed to update item'); + // reset the source xchan to prevent loops + $r = q("update item set source_xchan = '' where id = %d", + intval($item_id) + ); + } + return; } if ($group && (! $parent)) { @@ -3181,8 +3319,8 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false if ($r) { if (intval($item['item_deleted'])) { - drop_item($r[0]['id'], false, DROPITEM_PHASE1); - Master::Summon([ 'Notifier', 'drop', $r[0]['id'] ]); + drop_item($r[0]['id'], DROPITEM_PHASE1, uid: $r[0]['uid']); + Master::Summon(['Notifier', 'drop' ,$r[0]['id']]); return; } $arr['id'] = intval($r[0]['id']); @@ -3199,9 +3337,14 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false } else { - $arr['uuid'] = item_message_id(); + // To prevent duplicates from possible clones of the forum/group, + // we will create a v5 UUID of the source item mid. + // Add some extra entropy to prevent duplicate UUIDs with items where we already + // created an UUID from the mid (activities which do not provide an UUID field). + $arr['uuid'] = uuid_from_url($item['mid'] . '#group_item'); $arr['mid'] = z_root() . '/item/' . $arr['uuid']; $arr['parent_mid'] = $arr['mid']; + $arr['plink'] = $arr['mid']; } $arr['aid'] = $channel['channel_account_id']; @@ -3262,8 +3405,15 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false } $arr['title'] = $item['title']; - $arr['tgt_type'] = $item['tgt_type']; - $arr['target'] = $item['target']; +// $arr['tgt_type'] = $item['tgt_type']; +// $arr['target'] = $item['target']; + + $arr['tgt_type'] = 'Collection'; + $arr['target'] = [ + 'id' => str_replace('/item/', '/conversation/', $arr['parent_mid']), + 'type' => 'Collection', + 'attributedTo' => channel_url($channel['channel_address']) + ]; $arr['term'] = $item['term']; @@ -3286,10 +3436,14 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false $post = item_store($arr); } - $post_id = $post['item_id']; + $post_id = $post['item_id'] ?? 0; + $approval_id = $post['approval_id'] ?? 0; if($post_id) { Master::Summon([ 'Notifier','tgroup',$post_id ]); + if ($approval_id) { + Master::Summon(['Notifier', 'tgroup', $approval_id]); + } } return; } @@ -3314,14 +3468,14 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false if ($edit) { if (intval($item['item_deleted'])) { - drop_item($item['id'],false,DROPITEM_PHASE1); - Master::Summon([ 'Notifier','drop',$item['id'] ]); + drop_item($item['id'], DROPITEM_PHASE1, uid: $item['uid']); + Master::Summon(['Notifier', 'drop', $item['id']]); return; } return; } else { - $arr['uuid'] = item_message_id(); + $arr['uuid'] = uuid_from_url($item['mid']); $arr['mid'] = z_root() . '/activity/' . $arr['uuid']; $arr['parent_mid'] = $item['parent_mid']; //IConfig::Set($arr,'activitypub','context', str_replace('/item/','/conversation/',$item['parent_mid'])); @@ -3365,12 +3519,12 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false $arr['deny_gid'] = $channel['channel_deny_gid']; $arr['comment_policy'] = map_scope(PermissionLimits::Get($channel['channel_id'],'post_comments')); - $post = item_store($arr); - $post_id = $post['item_id']; + $post = item_store($arr, deliver: false, addAndSync: false); + $post_id = $post['item_id'] ?? 0; - if ($post_id) { - Master::Summon([ 'Notifier','tgroup',$post_id ]); - } + if ($post_id) { + Master::Summon(['Notifier', 'tgroup', $post_id]); + } q("update channel set channel_lastpost = '%s' where channel_id = %d", dbesc(datetime_convert()), @@ -3380,81 +3534,6 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false return; } - - // Change this copy of the post to a forum head message and deliver to all the tgroup members - // also reset all the privacy bits to the forum default permissions - - $private = (($channel['channel_allow_cid'] || $channel['channel_allow_gid'] - || $channel['channel_deny_cid'] || $channel['channel_deny_gid']) ? 1 : 0); - - $new_public_policy = map_scope(PermissionLimits::Get($channel['channel_id'],'view_stream'),true); - - if((! $private) && $new_public_policy) - $private = 1; - - $item_wall = 1; - $item_origin = (($item['item_deleted']) ? 0 : 1); // item_origin for deleted items is set to 0 in delete_imported_item() to prevent looping. In this case we probably should not set it back to 1 here. - $item_uplink = 0; - $item_nocomment = 0; - - $flag_bits = $item['item_flags']; - - // maintain the original source, which will be the original item owner and was stored in source_xchan - // when we created the delivery fork - - if($parent) { - $r = q("update item set source_xchan = '%s' where id = %d", - dbesc($parent['source_xchan']), - intval($item_id) - ); - } - else { - $item_uplink = (($item['item_rss']) ? 0 : 1); // Do not set item_uplink for rss items - we can not send anything to them. - - // if this is an edit, item_store_update() will have already updated the item - // with the correct value for source_xchan (by ignoring it). We cannot set to owner_xchan - // in this case because owner_xchan will point to the parent of this chain - // and not the original sender. - - if(! $edit) { - $r = q("update item set source_xchan = owner_xchan where id = %d", - intval($item_id) - ); - } - } - - $title = $item['title']; - $body = $item['body']; - - $r = q("update item set item_uplink = %d, item_nocomment = %d, item_flags = %d, owner_xchan = '%s', allow_cid = '%s', allow_gid = '%s', - deny_cid = '%s', deny_gid = '%s', item_private = %d, public_policy = '%s', comment_policy = '%s', title = '%s', body = '%s', item_wall = %d, item_origin = %d where id = %d", - intval($item_uplink), - intval($item_nocomment), - intval($flag_bits), - dbesc($channel['channel_hash']), - dbesc($channel['channel_allow_cid']), - dbesc($channel['channel_allow_gid']), - dbesc($channel['channel_deny_cid']), - dbesc($channel['channel_deny_gid']), - intval($private), - dbesc($new_public_policy), - dbesc(map_scope(PermissionLimits::Get($channel['channel_id'],'post_comments'))), - dbesc($title), - dbesc($body), - intval($item_wall), - intval($item_origin), - intval($item_id) - ); - - if($r) - Master::Summon([ 'Notifier','tgroup',$item_id ]); - else { - logger('start_delivery_chain: failed to update item'); - // reset the source xchan to prevent loops - $r = q("update item set source_xchan = '' where id = %d", - intval($item_id) - ); - } } /** @@ -3793,7 +3872,7 @@ function item_expire($uid,$days,$comment_days = 7) { if ($r) { foreach ($r as $item) { - drop_item($item['id'], false); + drop_item($item['id'], uid: $uid); } } @@ -3806,25 +3885,12 @@ function retain_item($id) { ); } -function drop_items($items,$interactive = false,$stage = DROPITEM_NORMAL) { - $uid = 0; - - if(! local_channel() && ! remote_channel()) - return; - - if(count($items)) { +function drop_items($items, $stage = DROPITEM_NORMAL, $force = false, $expire = false) { + if ($items) { foreach($items as $item) { - $owner = drop_item($item,$interactive,$stage); - if($owner && ! $uid) - $uid = $owner; + drop_item($item, $stage, $force, expire: $expire); } } - - // multiple threads may have been deleted, send an expire notification - - if($uid) { - Master::Summon([ 'Notifier','expire',$uid ]); - } } @@ -3837,7 +3903,7 @@ function drop_items($items,$interactive = false,$stage = DROPITEM_NORMAL) { // $stage = 1 => set deleted flag on the item and perform intial notifications // $stage = 2 => perform low level delete at a later stage -function drop_item($id,$interactive = true,$stage = DROPITEM_NORMAL) { +function drop_item($id, $stage = DROPITEM_NORMAL, $force = false, $uid = 0, $observer_hash = '', $expire = false, $recurse = false) { // locate item to be deleted @@ -3845,33 +3911,48 @@ function drop_item($id,$interactive = true,$stage = DROPITEM_NORMAL) { intval($id) ); - if((! $r) || (intval($r[0]['item_deleted']) && ($stage === DROPITEM_NORMAL))) { - if(! $interactive) - return 0; - notice( t('Item not found.') . EOL); - //goaway(z_root() . '/' . $_SESSION['return_url']); + if(!$r || (intval($r[0]['item_deleted']) && $stage === DROPITEM_NORMAL)) { + return false; } $item = $r[0]; - $ok_to_delete = false; + if(!$recurse) { + drop_related($item, $stage, $force, $uid, $observer_hash, $expire); + } - // system deletion - if(! $interactive) - $ok_to_delete = true; + $ok_to_delete = false; // admin deletion - if(is_site_admin()) + if(is_site_admin()) { $ok_to_delete = true; + } // owner deletion - if(local_channel() && local_channel() == $item['uid']) + if(local_channel() && local_channel() == $item['uid']) { $ok_to_delete = true; + } + + // remote delete when nobody is authenticated (called from Libzot and Daemons) + if ($uid && intval($uid) === intval($item['uid'])) { + $ok_to_delete = true; + } // author deletion - $observer = App::get_observer(); - if($observer && $observer['xchan_hash'] && ($observer['xchan_hash'] === $item['author_xchan'])) + if ($observer_hash) { + $observer = ['xchan_hash' => $observer_hash]; + } + else { + $observer = App::get_observer(); + } + + if (isset($observer['xchan_hash']) && $observer['xchan_hash'] === $item['author_xchan']) { $ok_to_delete = true; + } + + if (isset($observer['xchan_hash']) && $observer['xchan_hash'] === $item['owner_xchan']) { + $ok_to_delete = true; + } if($ok_to_delete) { @@ -3884,9 +3965,9 @@ function drop_item($id,$interactive = true,$stage = DROPITEM_NORMAL) { $arr = [ 'item' => $item, - 'interactive' => $interactive, 'stage' => $stage ]; + /** * @hooks drop_item * Called when an 'item' is removed. @@ -3909,30 +3990,95 @@ function drop_item($id,$interactive = true,$stage = DROPITEM_NORMAL) { delete_item_lowlevel($item, $stage); } - if(! $interactive) - return 1; + return true; + } + else { + return false; + } +} - // send the notification upstream/downstream as the case may be - // only send notifications to others if this is the owner's wall item. - // This isn't optimal. We somehow need to pass to this function whether or not - // to call the notifier, or we need to call the notifier from the calling function. - // We'll rely on the undocumented behaviour that DROPITEM_PHASE1 is (hopefully) only - // set if we know we're going to send delete notifications out to others. +// If somebody deletes a 'Create' activity, find any associated 'Add/Collection' +// activity and delete it. And vice versa. - if((intval($item['item_wall']) && ($stage != DROPITEM_PHASE2)) || ($stage == DROPITEM_PHASE1)) { - Master::Summon([ 'Notifier','drop',$notify_id ]); +function drop_related($item, $stage = DROPITEM_NORMAL, $force = false, $uid = 0, $observer_hash = '', $expire = false, $recurse = false) { + $allRelated = q("select * from item where parent_mid = '%s' and uid = %d", + dbesc($item['parent_mid']), + intval($item['uid']) + ); + if (! $allRelated) { + return; + } + if ($item['verb'] === 'Add' && $item['tgt_type'] === 'Collection') { + if (is_array($item['obj'])) { + $thisItem = $item['obj']; + } + else { + $thisItem = json_decode($item['obj'], true); + } + if (isset($thisItem['object']['id'])) { + $targetMid = $thisItem['object']['id']; + } + if (!$targetMid) { + return; + } + foreach ($allRelated as $related) { + if ($related['mid'] === $targetMid) { + drop_item($related['id'], $stage, $force, $uid, $observer_hash, $expire, recurse: true); + break; + } } - //goaway(z_root() . '/' . $_SESSION['return_url']); } else { - if(! $interactive) - return 0; - notice( t('Permission denied.') . EOL); - //goaway(z_root() . '/' . $_SESSION['return_url']); + foreach ($allRelated as $related) { + if ($related['verb'] === 'Add' && str_contains($related['tgt_type'], 'Collection')) { + $thisItem = json_decode($related['obj'], true); + if (isset($thisItem['id']) && $thisItem['id'] === str_replace('/item/', '/activity/', $item['mid'])) { + drop_item($related['id'], $stage, $force, $uid, $observer_hash, $expire, recurse: true); + break; + } + } + } + } +} + + +function find_related($item) { + $allRelated = q("select * from item where parent_mid = '%s' and uid = %d", + dbesc($item['parent_mid']), + intval($item['uid']) + ); + if (! $allRelated) { + return false; } + if ($item['verb'] === 'Add' && $item['tgt_type'] === 'Collection') { + $thisItem = json_decode($item['obj'],true); + if (is_array($thisItem)) { + $targetMid = $thisItem['object']['id']; + } + if (!$targetMid) { + return false; + } + foreach ($allRelated as $related) { + if ($related['mid'] === $targetMid) { + return $related; + } + } + } + else { + foreach ($allRelated as $related) { + if ($related['verb'] === 'Add' && str_contains($related['tgt_type'], 'Collection')) { + $thisItem = json_decode($related['obj'], true); + if (isset($thisItem['object']['id']) && $thisItem['object']['id'] === $item['mid']) { + return $related; + } + } + } + } + return false; } + /** * @warning This function does not check for permission and does not send * notifications and does not check recursion. @@ -4656,19 +4802,19 @@ function items_fetch($arr,$channel = null,$observer_hash = null,$client_mode = C return $items; } -function webpage_to_namespace($webpage) { +function item_type_to_namespace($item_type) { - if($webpage == ITEM_TYPE_WEBPAGE) + if($item_type == ITEM_TYPE_WEBPAGE) $page_type = 'WEBPAGE'; - elseif($webpage == ITEM_TYPE_BLOCK) + elseif($item_type == ITEM_TYPE_BLOCK) $page_type = 'BUILDBLOCK'; - elseif($webpage == ITEM_TYPE_PDL) + elseif($item_type == ITEM_TYPE_PDL) $page_type = 'PDL'; - elseif($webpage == ITEM_TYPE_CARD) + elseif($item_type == ITEM_TYPE_CARD) $page_type = 'CARD'; - elseif($webpage == ITEM_TYPE_ARTICLE) + elseif($item_type == ITEM_TYPE_ARTICLE) $page_type = 'ARTICLE'; - elseif($webpage == ITEM_TYPE_DOC) + elseif($item_type == ITEM_TYPE_DOC) $page_type = 'docfile'; else $page_type = 'unknown'; @@ -4677,12 +4823,12 @@ function webpage_to_namespace($webpage) { } -function update_remote_id($channel,$post_id,$webpage,$pagetitle,$namespace,$remote_id,$mid) { +function update_remote_id($channel,$post_id,$item_type,$pagetitle,$namespace,$remote_id,$mid) { if(! intval($post_id)) return; - $page_type = webpage_to_namespace($webpage); + $page_type = item_type_to_namespace($item_type); if($page_type == 'unknown' && $namespace && $remote_id) { $page_type = $namespace; @@ -5008,7 +5154,7 @@ function fix_attached_permissions($uid, $body, $str_contact_allow, $str_group_al $attach = array_shift($attach_q); - $new_public = !(($str_contact_allow || $str_group_allow || $str_contact_deny || $str_group_deny)); + //$new_public = !(($str_contact_allow || $str_group_allow || $str_contact_deny || $str_group_deny)); $existing_public = !(($attach['allow_cid'] || $attach['allow_gid'] || $attach['deny_cid'] || $attach['deny_gid'])); if ($existing_public) { @@ -5023,10 +5169,11 @@ function fix_attached_permissions($uid, $body, $str_contact_allow, $str_group_al continue; } - $item_private = 0; - - if ($new_public === false) { - $item_private = (($str_group_allow || ($str_contact_allow && substr_count($str_contact_allow,'<') > 2)) ? 1 : 2); + if ($token) { + $str_contact_allow = $attach['allow_cid']; + $str_group_allow = $attach['allow_gid']; + $str_contact_deny = $attach['deny_cid']; + $str_group_deny = $attach['deny_gid']; // preserve any existing tokens that may have been set for this file $token_matches = null; @@ -5054,7 +5201,7 @@ function fix_attached_permissions($uid, $body, $str_contact_allow, $str_group_al ); if ($attach['is_photo']) { - $r = q("UPDATE photo SET allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s' + q("UPDATE photo SET allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s' WHERE resource_id = '%s' AND uid = %d ", dbesc($str_contact_allow), dbesc($str_group_allow), @@ -5064,7 +5211,16 @@ function fix_attached_permissions($uid, $body, $str_contact_allow, $str_group_al intval($uid) ); - $r = q("UPDATE item SET allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s', item_private = %d + $item_private = 0; + + if ($str_group_allow || $str_contact_deny || $str_group_deny) { + $item_private = 1; + } + elseif ($str_contact_allow) { + $item_private = 2; + } + + q("UPDATE item SET allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s', item_private = %d WHERE resource_id = '%s' AND 'resource_type' = 'photo' AND uid = %d", dbesc($str_contact_allow), dbesc($str_group_allow), @@ -5085,7 +5241,7 @@ function fix_attached_permissions($uid, $body, $str_contact_allow, $str_group_al * which will allow you to interact with it. */ -function copy_of_pubitem($channel,$mid) { +function copy_of_pubitem($channel, $mid) { $result = null; $syschan = get_sys_channel(); @@ -5103,36 +5259,517 @@ function copy_of_pubitem($channel,$mid) { return $item[0]; } - - $r = q("select * from item where parent_mid = (select parent_mid from item where mid = '%s' and uid = %d ) order by id ", + $r = q("select * from item where parent_mid = (select parent_mid from item where mid = '%s' and uid = %d) and uid = %d order by id ", dbesc($mid), + intval($syschan['channel_id']), intval($syschan['channel_id']) ); if($r) { $items = fetch_post_tags($r,true); foreach($items as $rv) { - $d = q("select id from item where mid = '%s' and uid = %d limit 1", - dbesc($rv['mid']), - intval($channel['channel_id']) - ); - if($d) { - continue; - } unset($rv['id']); unset($rv['parent']); + $rv['aid'] = $channel['channel_account_id']; $rv['uid'] = $channel['channel_id']; $rv['item_wall'] = 0; $rv['item_origin'] = 0; - $x = item_store($rv); + $x = item_store($rv, deliver: false, addAndSync: false); if($x['item_id'] && $x['item']['mid'] === $mid) { $result = $x['item']; + sync_an_item($channel['channel_id'], $x['item_id']); } } } + return $result; } + +function addToCollectionAndSync($ret) { + if (!$ret['success']) { + return $ret; + } + + $channel = channelx_by_n($ret['item']['uid']); + if ($channel && $channel['channel_hash'] === $ret['item']['owner_xchan']) { + $items = [$ret['item']]; + + if ((int)$items[0]['item_blocked'] === ITEM_MODERATED + || (int)$items[0]['item_unpublished'] || (int)$items[0]['item_delayed']) { + return $ret; + } + + xchan_query($items); + // TODO: fetch_post_tags() will add term and iconfig twice if called twice and it looks like they are already added here + // $items = fetch_post_tags($items); + $sync_items = []; + $sync_items[] = encode_item($items[0], true); + + if (!in_array($ret['item']['verb'], ['Add', 'Remove'])) { + $activity = Activity::encode_activity($items[0]); + + if (!$activity) { + return $ret; + } + + $new_obj = Activity::build_packet($activity, $channel, false); + $approval = Activity::addToCollection($channel, $new_obj, $ret['item']['parent_mid'], $ret['item'], deliver: false); + + if ($approval['success']) { + $ret['approval_id'] = $approval['item_id']; + $ret['approval'] = $approval['activity']; + $add_items = [$approval['activity']]; + xchan_query($add_items); + $add_items = fetch_post_tags($add_items); + $sync_items[] = encode_item($add_items[0], true); + } + } + + $resource_type = $ret['item']['resource_type']; + + if ($resource_type === 'event') { + $z = q("select * from event where event_hash = '%s' and uid = %d limit 1", + dbesc($ret['item']['resource_id']), + intval($channel['channel_id']) + ); + + if ($z) { + Libsync::build_sync_packet($channel['channel_id'], ['event_item' => $sync_items, 'event' => $z]); + } + } + elseif ($resource_type === 'photo') { + // reserved for future use, currently handled in the photo upload workflow + } + else { + Libsync::build_sync_packet($ret['item']['uid'], ['item' => $sync_items]); + } + } + + return $ret; +} + +function reverse_activity_mid($string) { + return str_replace(z_root() . '/activity/', z_root() . '/item/', $string); +} + +function set_activity_mid($string) { + return str_replace(z_root() . '/item/', z_root() . '/activity/', $string); +} + +/** + * @brief returns an item by id and parent belonging to local_channel() + * including activity counts. + * @param int $id + * @param int $parent + */ + +function item_by_item_id(int $id, int $parent): array +{ + if (!$id && !$parent && !local_channel()) { + return []; + } + + $item_normal_sql = item_normal(); + + $reaction = item_reaction_sql($parent); + $reaction_cte_sql = $reaction['cte']; + $reaction_select_sql = $reaction['select']; + $reaction_join_sql = $reaction['join']; + + return q("WITH + $reaction_cte_sql + SELECT + *, + $reaction_select_sql + FROM item + $reaction_join_sql + WHERE + item.id = %d + AND item.uid = %d + AND item.verb IN ('Create', 'Update', 'EmojiReact') + $item_normal_sql", + intval($id), + intval(local_channel()) + ); +} + + +/** + * @brief returns an array of items by ids + * ATTENTION: no permissions for the parents are checked here!!! + * Permissions MUST be checked by the module which calls this function. + * @param array $parents + * @param null|array $thr_parents (optional) - thr_parent mids which will be included + * @param string $permission_sql (optional) - SQL as provided by item_permission_sql() from the calling module + * @param bool $blog_mode (optional) - if set to yes only the parent items will be returned + */ + +function items_by_parent_ids(array $parents, null|array $thr_parents = null, string $permission_sql = '', bool $blog_mode = false): array +{ + if (!$parents) { + return []; + } + + $ids = ids_to_querystr($parents, 'item_id'); + $thread_allow = ((local_channel()) ? PConfig::Get(local_channel(), 'system', 'thread_allow', true) : Config::Get('system', 'thread_allow', true)); + $item_normal_sql = item_normal(); + $limit = $thread_allow ? 3 : 1000; + + $thr_parent_sql = (($thread_allow) ? " AND item.thr_parent = item.parent_mid " : ''); + if ($thr_parents && $thread_allow) { + $limit = 300; + $thr_parent_str = stringify_array($thr_parents, true); + $thr_parent_sql = " AND item.thr_parent IN (" . protect_sprintf($thr_parent_str) . ") "; + } + + $reaction = item_reaction_sql($ids, $permission_sql, 'final_selection'); + $reaction_cte_sql = $reaction['cte']; + $reaction_select_sql = $reaction['select']; + $reaction_join_sql = $reaction['join']; + + if ($blog_mode) { + $q = <<<SQL + WITH + final_selection AS ( + SELECT + item.* + FROM + item + WHERE + item.id IN ($ids) + ), + + $reaction_cte_sql + + SELECT + final_selection.*, + $reaction_select_sql + FROM final_selection + $reaction_join_sql + SQL; + + return dbq(trim($q)); + } + + $q = <<<SQL + WITH + parent_items AS ( + SELECT + item.*, + 0 AS rn + FROM item + WHERE + item.id IN ($ids) + ), + + $reaction_cte_sql, + + all_comments AS ( + SELECT + item.*, + ROW_NUMBER() OVER (PARTITION BY item.parent ORDER BY item.created DESC) AS rn + FROM item + WHERE item.parent IN ($ids) + AND item.verb IN ('Create', 'Update', 'EmojiReact') + AND item.item_thread_top = 0 + $thr_parent_sql + $permission_sql + $item_normal_sql + ), + + final_selection AS ( + SELECT * FROM parent_items + UNION ALL + SELECT * FROM all_comments WHERE all_comments.rn <= $limit + ) + + SELECT + final_selection.*, + $reaction_select_sql + FROM final_selection + $reaction_join_sql + SQL; + + return dbq(trim($q)); +} + +/** + * @brief prepare reaction sql for items_by_parent_ids() + * ATTENTION: no permissions for the pa are checked here!!! + * Permissions MUST be checked by the function which returns the ids. + * @param string $ids + * @param string $permission_sql (optional) - SQL provided by item_permission_sql() + * @param string $join_prefix (optional) - prefix for the join part defaults to 'item' + */ + +function item_reaction_sql(string $ids, string $permission_sql = '', string $join_prefix = 'item'): array +{ + $item_normal_sql = item_normal(); + $observer = get_observer_hash(); + + $verbs = [ + 'like' => ['Like'], + 'dislike' => ['Dislike'], + 'announce' => ['Announce'], + 'accept' => ['Accept'], + 'reject' => ['Reject'], + 'tentativeaccept' => ['TentativeAccept'] + ]; + + $thread_allow = ((local_channel()) ? PConfig::Get(local_channel(), 'system', 'thread_allow', true) : Config::Get('system', 'thread_allow', true)); + + if ($thread_allow) { + $verbs['comment'] = ['Create', 'Update', 'EmojiReact']; + } + + $cte = ''; + $select = ''; + $join = ''; + + foreach($verbs as $k => $v) { + + $observer_sql = "0 AS observer_{$k}_count"; + if ($observer) { + $observer_sql = "COUNT(CASE WHEN item.author_xchan = '$observer' THEN 1 END) AS observer_{$k}_count"; + } + + $verbs_str = stringify_array($v); + + if ($cte) { + $cte .= ",\n"; + } + + $cte .= <<<SQL + reaction_{$k} AS ( + SELECT + item.thr_parent, + -- COUNT(DISTINCT item.author_xchan) AS {$k}_count, (should we prevent multiple reactions by the same author?) + COUNT(*) AS {$k}_count, + $observer_sql + FROM item + WHERE item.verb IN ($verbs_str) + AND item.item_thread_top = 0 + AND item.parent IN ($ids) + $item_normal_sql + $permission_sql + GROUP BY item.thr_parent + ) + SQL; + + if ($select) { + $select .= ",\n"; + } + + $select .= <<<SQL + COALESCE(reaction_{$k}.{$k}_count, 0) AS {$k}_count, + COALESCE(reaction_{$k}.observer_{$k}_count, 0) AS observer_{$k}_count + SQL; + + $join .= <<<SQL + LEFT JOIN reaction_{$k} ON reaction_{$k}.thr_parent = $join_prefix.mid + SQL; + + } + + $ret['cte'] = $cte; + $ret['select'] = $select; + $ret['join'] = $join; + + return $ret; +} + + + +/** + * @brief returns an array of items by thr_parent mid of a parent + + * @param string $mid + * @param int $parent + * @param int|null $offset + */ + +function items_by_thr_parent(string $mid, int $parent, int|null $offset = null): array +{ + if (!$mid && !$parent) { + return []; + } + + $parent_item = q("SELECT uid FROM item WHERE id = %d", + intval($parent) + ); + + $order_sql = "ORDER BY item.created"; + if (isset($offset)) { + $order_sql = "ORDER BY item.created DESC, item.received DESC LIMIT 3 OFFSET $offset"; + } + + $owner_uid = intval($parent_item[0]['uid']); + $item_normal_sql = item_normal($owner_uid); + + if (local_channel() === $owner_uid) { + $reaction = item_reaction_sql($parent); + $reaction_cte_sql = $reaction['cte']; + $reaction_select_sql = $reaction['select']; + $reaction_join_sql = $reaction['join']; + + $ret = q("WITH + $reaction_cte_sql + SELECT + item.*, + $reaction_select_sql + FROM item + $reaction_join_sql + WHERE + item.thr_parent = '%s' + AND item.uid = %d + AND item.verb IN ('Create', 'Update', 'EmojiReact') + AND item.item_thread_top = 0 + $item_normal_sql + $order_sql", + dbesc($mid), + intval($owner_uid) + ); + } + else { + $observer_hash = get_observer_hash(); + $permission_sql = item_permissions_sql($owner_uid, $observer_hash); + + $reaction = item_reaction_sql($parent, $permission_sql); + $reaction_cte_sql = $reaction['cte']; + $reaction_select_sql = $reaction['select']; + $reaction_join_sql = $reaction['join']; + + $ret = q("WITH + $reaction_cte_sql + SELECT + item.*, + $reaction_select_sql + FROM item + $reaction_join_sql + WHERE + item.thr_parent = '%s' + AND item.uid = %d + AND item.verb IN ('Create', 'Update', 'EmojiReact') + AND item.item_thread_top = 0 + $permission_sql + $item_normal_sql + $order_sql", + dbesc($mid), + intval($owner_uid) + ); + } + + if (isset($offset)) { + $ret = array_reverse($ret); + } + + return $ret; +} + + +/** + * @brief returns an array of xchan entries (partly) for activities of an item by mid of a parent. + * Also checks if observer is allowed to add activities to the item. + * @param string $mid + * @param int $parent + * @param string $verb + */ + +function item_activity_xchans(string $mid, int $parent, string $verb): array +{ + if (!$mid && !$parent && !$verb) { + return []; + } + + $observer_hash = get_observer_hash(); + $parent_item = q("SELECT * FROM item WHERE id = %d", + intval($parent) + ); + + $owner_uid = intval($parent_item[0]['uid']); + $item_normal = item_normal($owner_uid); + + if (local_channel() === $owner_uid) { + $ret = q("SELECT item.id, item.item_blocked, xchan.xchan_hash, xchan.xchan_name as name, xchan.xchan_url as url, xchan.xchan_photo_s as photo FROM item + LEFT JOIN xchan ON item.author_xchan = xchan.xchan_hash + WHERE item.uid = %d + AND item.parent = %d + AND item.thr_parent = '%s' + AND item.verb = '%s' + AND item.item_thread_top = 0 + $item_normal + -- GROUP BY item.author_xchan (should we prevent multiple reactions by the same author?) + ORDER BY item.created", + intval(local_channel()), + intval($parent), + dbesc($mid), + dbesc($verb) + ); + } + else { + $sql_extra = item_permissions_sql($owner_uid, $observer_hash); + + $ret = q("SELECT item.id, item.item_blocked, xchan.xchan_hash, xchan.xchan_name as name, xchan.xchan_url as url, xchan.xchan_photo_s as photo FROM item + LEFT JOIN xchan ON item.author_xchan = xchan.xchan_hash + WHERE item.uid = %d + AND item.thr_parent = '%s' + AND item.verb = '%s' + AND item.item_thread_top = 0 + $sql_extra + $item_normal + -- GROUP BY item.author_xchan (should we prevent multiple reactions by the same author?) + ORDER BY item.created", + intval($owner_uid), + dbesc($mid), + dbesc($verb) + ); + } + + $ret['is_commentable'] = can_comment_on_post($observer_hash, $parent_item[0]); + + return $ret; +} + + +/** + * @brief find and return thr_parents we need to show when displaying a nested comment. + * TODO: can this be improved or maybe implemented differently in the UI? + * @param array $item + */ + +function get_recursive_thr_parents(array $item): array|null +{ + if ($item['id'] === $item['parent']) { + // This is a toplevel post, return null. + return null; + } + + $thr_parents[] = $item['thr_parent']; + + $mid = $item['thr_parent']; + $parent_mid = $item['parent_mid']; + $uid = $item['uid']; + $i = 0; + + while ($mid !== $item['parent_mid'] && $i < 100) { + $x = q("SELECT thr_parent, mid FROM item WHERE uid = %d AND mid = '%s'", + intval($uid), + dbesc($mid) + ); + + if (!$x) { + break; + } + + $mid = $x[0]['thr_parent']; + $thr_parents[] = $x[0]['thr_parent']; + + $i++; + } + + return $thr_parents; +} diff --git a/include/js_strings.php b/include/js_strings.php index 090d28ce3..6f2ffd351 100644 --- a/include/js_strings.php +++ b/include/js_strings.php @@ -5,10 +5,8 @@ function js_strings() { '$delitem' => t('Delete this item?'), '$itemdel' => t('Item deleted'), '$comment' => t('Comment'), - '$showmore' => sprintf( t('%s show all'), '<i class=\'fa fa-chevron-down\'></i>'), - '$showfewer' => sprintf( t('%s show less'), '<i class=\'fa fa-chevron-up\'></i>'), - '$divgrowmore' => sprintf( t('%s expand'), '<i class=\'fa fa-chevron-down\'></i>'), - '$divgrowless' => sprintf( t('%s collapse'),'<i class=\'fa fa-chevron-up\'></i>'), + '$divgrowmore' => t('expand'), + '$divgrowless' => t('collapse'), '$pwshort' => t("Password too short"), '$pwnomatch' => t("Passwords do not match"), '$everybody' => t('everybody'), @@ -38,6 +36,7 @@ function js_strings() { '$pinned' => t('Pinned'), '$pin_item' => t('Pin to the top'), '$unpin_item' => t('Unpin from the top'), + '$dblclick_to_exit_zoom' => t('Double click to exit zoom'), // translatable prefix and suffix strings for jquery.timeago - // using the defaults set below if left untranslated, empty strings if @@ -49,29 +48,6 @@ function js_strings() { 'months' => tt('%d months', '%d months', '%d'), 'years' => tt('%d years', '%d years', '%d'), - // get plural function code - 'plural_func' => tf(), - - '$t01' => ((t('timeago.prefixAgo') == 'timeago.prefixAgo') ? '' : ((t('timeago.prefixAgo') == 'NONE') ? '' : t('timeago.prefixAgo'))), - '$t02' => ((t('timeago.prefixFromNow') == 'timeago.prefixFromNow') ? '' : ((t('timeago.prefixFromNow') == 'NONE') ? '' : t('timeago.prefixFromNow'))), - '$t03' => ((t('timeago.suffixAgo') == 'timeago.suffixAgo') ? 'ago' : ((t('timeago.suffixAgo') == 'NONE') ? '' : t('timeago.suffixAgo'))), - '$t04' => ((t('timeago.suffixFromNow') == 'timeago.suffixFromNow') ? 'from now' : ((t('timeago.suffixFromNow') == 'NONE') ? '' : t('timeago.suffixFromNow'))), - - // translatable main strings for jquery.timeago - '$t05' => t('less than a minute'), - '$t06' => t('about a minute'), - '$t07' => ta('%d minutes'), - '$t08' => t('about an hour'), - '$t09' => ta('about %d hours'), - '$t10' => t('a day'), - '$t11' => ta('%d days'), - '$t12' => t('about a month'), - '$t13' => ta('%d months'), - '$t14' => t('about a year'), - '$t15' => ta('%d years'), - '$t16' => t(' '), // wordSeparator - '$t17' => ((t('timeago.numbers') != 'timeago.numbers') ? t('timeago.numbers') : '[]'), - '$January' => t('January'), '$February' => t('February'), '$March' => t('March'), diff --git a/include/language.php b/include/language.php index 538f67d90..9b68717f8 100644 --- a/include/language.php +++ b/include/language.php @@ -29,7 +29,7 @@ function get_browser_language() { $langs = []; $lang_parse = []; - if (x($_SERVER, 'HTTP_ACCEPT_LANGUAGE')) { + if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // break up string into pieces (languages and q factors) preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $lang_parse); @@ -204,7 +204,7 @@ function load_translation_table($lang, $install = false) { function t($s, $ctx = ''): string { $cs = $ctx ? '__ctx:' . $ctx . '__ ' . $s : $s; - if (x(App::$strings, $cs)) { + if (!empty(App::$strings[$cs])) { $t = App::$strings[$cs]; return ((is_array($t)) ? translate_projectname($t[0]) : translate_projectname($t)); @@ -219,7 +219,7 @@ function t($s, $ctx = ''): string { */ function translate_projectname($s) { - if(strpos($s,'rojectname') !== false) { + if(str_contains($s,'rojectname')) { return str_replace(array('$projectname','$Projectname'),array(Zotlabs\Lib\System::get_platform_name(),ucfirst(Zotlabs\Lib\System::get_platform_name())),$s); } return $s; @@ -239,7 +239,7 @@ function translate_projectname($s) { function tt($singular, $plural, $count, $ctx = ''){ $cs = $ctx ? "__ctx:" . $ctx . "__ " . $singular : $singular; - if (x(App::$strings,$cs)) { + if (!empty(App::$strings[$cs])) { $t = App::$strings[$cs]; $f = 'string_plural_select_' . str_replace('-', '_', App::$language); if (! function_exists($f)) @@ -282,9 +282,8 @@ function ta($k){ */ function tf() { - - $s = "plural_function_code"; - return (x(App::$strings, $s) ? App::$strings[$s] : "0"); + $s = "plural_function_code"; + return (!empty(App::$strings[$s]) ? App::$strings[$s] : "0"); } /** @@ -391,7 +390,6 @@ function language_list() { $langs = glob('view/*/hstrings.php'); $lang_options = array(); - $selected = ""; if(is_array($langs) && count($langs)) { if(! in_array('view/en/hstrings.php',$langs)) @@ -406,41 +404,6 @@ function language_list() { return $lang_options; } -function lang_selector() { - - $langs = glob('view/*/hstrings.php'); - - $lang_options = array(); - $selected = ""; - - if(is_array($langs) && count($langs)) { - $langs[] = ''; - if(! in_array('view/en/hstrings.php',$langs)) - $langs[] = 'view/en/'; - asort($langs); - foreach($langs as $l) { - if($l == '') { - $lang_options[""] = t('default'); - continue; - } - $ll = substr($l,5); - $ll = substr($ll,0,strrpos($ll,'/')); - $selected = (($ll === App::$language && (x($_SESSION, 'language'))) ? $ll : $selected); - $lang_options[$ll] = get_language_name($ll, $ll) . " ($ll)"; - } - } - - $tpl = get_markup_template('lang_selector.tpl'); - - $o = replace_macros($tpl, array( - '$title' => t('Select an alternate language'), - '$langs' => array($lang_options, $selected), - - )); - - return $o; -} - function rtl_languages() { return [ 'ar', diff --git a/include/markdown.php b/include/markdown.php index b2adcd0d5..90d671fe4 100644 --- a/include/markdown.php +++ b/include/markdown.php @@ -80,22 +80,6 @@ function markdown_to_bb($s, $use_zrl = false, $options = []) { $s = html2bbcode($s); - // $s = bb_code_protect($s); - - // Convert everything that looks like a link to a link - if($use_zrl) { - if (strpos($s,'[/img]') !== false) { - $s = preg_replace_callback("/\[img\](.*?)\[\/img\]/ism", 'use_zrl_cb_img', $s); - $s = preg_replace_callback("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", 'use_zrl_cb_img_x', $s); - } - $s = preg_replace_callback("/([^\]\=\{\/]|^)(https?\:\/\/)([a-zA-Z0-9\pL\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,\@\(\)]+)([\,\.\:\;]\s|$)/ismu", 'use_zrl_cb_link',$s); - } - else { - $s = preg_replace("/([^\]\=\{\/]|^)(https?\:\/\/)([a-zA-Z0-9\pL\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,\@\(\)]+)([\,\.\:\;]\s|$)/ismu", '$1[url=$2$3]$2$3[/url]$4',$s); - } - - // $s = bb_code_unprotect($s); - // remove duplicate adjacent code tags $s = preg_replace("/(\[code\])+(.*?)(\[\/code\])+/ism","[code]$2[/code]", $s); diff --git a/include/nav.php b/include/nav.php index a68d564a0..3426db5f2 100644 --- a/include/nav.php +++ b/include/nav.php @@ -1,8 +1,8 @@ <?php /** @file */ -use \Zotlabs\Lib\Apps; -use \Zotlabs\Lib\Chatroom; -use \Zotlabs\Lib\Config; +use Zotlabs\Lib\Apps; +use Zotlabs\Lib\Chatroom; +use Zotlabs\Lib\Config; require_once('include/security.php'); require_once('include/menu.php'); @@ -366,7 +366,7 @@ function nav($template = 'default') { '$form_security_token' => get_form_security_token('pconfig') ]); - if (x($_SESSION, 'reload_avatar') && $observer) { + if (!empty($_SESSION['reload_avatar']) && $observer) { // The avatar has been changed on the server but the browser doesn't know that, // force the browser to reload the image from the server instead of its cache. $tpl = get_markup_template('force_image_reload.tpl'); @@ -449,7 +449,7 @@ function channel_apps($is_owner = false, $nickname = null) { 'sel' => ((argv(0) == 'channel') ? 'active' : ''), 'title' => t('Status Messages and Posts'), 'id' => 'status-tab', - 'icon' => 'home' + 'icon' => 'house' ], ]; @@ -462,7 +462,7 @@ function channel_apps($is_owner = false, $nickname = null) { 'sel' => ((argv(0) == 'profile') ? 'active' : ''), 'title' => t('Profile Details'), 'id' => 'profile-tab', - 'icon' => 'user' + 'icon' => 'person' ]; } if ($p['view_storage']) { @@ -472,7 +472,7 @@ function channel_apps($is_owner = false, $nickname = null) { 'sel' => ((argv(0) == 'photos') ? 'active' : ''), 'title' => t('Photo Albums'), 'id' => 'photo-tab', - 'icon' => 'photo' + 'icon' => 'image' ]; $tabs[] = [ 'label' => t('Files'), @@ -480,7 +480,7 @@ function channel_apps($is_owner = false, $nickname = null) { 'sel' => ((argv(0) == 'cloud' || argv(0) == 'sharedwithme') ? 'active' : ''), 'title' => t('Files and Storage'), 'id' => 'files-tab', - 'icon' => 'folder-open' + 'icon' => 'folder' ]; } @@ -491,7 +491,7 @@ function channel_apps($is_owner = false, $nickname = null) { 'sel' => ((argv(0) == 'cal') ? 'active' : ''), 'title' => t('Calendar'), 'id' => 'event-tab', - 'icon' => 'calendar' + 'icon' => 'calendar-date' ]; } @@ -505,7 +505,7 @@ function channel_apps($is_owner = false, $nickname = null) { 'sel' => ((argv(0) == 'chat') ? 'active' : ''), 'title' => t('Chatrooms'), 'id' => 'chat-tab', - 'icon' => 'comments-o' + 'icon' => 'chat' ]; } } @@ -529,18 +529,7 @@ function channel_apps($is_owner = false, $nickname = null) { 'sel' => ((argv(0) == 'webpages') ? 'active' : ''), 'title' => t('View Webpages'), 'id' => 'webpages-tab', - 'icon' => 'newspaper-o' - ]; - } - - if ($p['view_wiki'] && Apps::system_app_installed($uid, 'Wiki')) { - $tabs[] = [ - 'label' => t('Wikis'), - 'url' => z_root() . '/wiki/' . $nickname, - 'sel' => ((argv(0) == 'wiki') ? 'active' : ''), - 'title' => t('Wiki'), - 'id' => 'wiki-tab', - 'icon' => 'pencil-square-o' + 'icon' => 'layout-text-sidebar' ]; } diff --git a/include/network.php b/include/network.php index b3a3d715c..83bb281a4 100644 --- a/include/network.php +++ b/include/network.php @@ -2,6 +2,7 @@ use Zotlabs\Lib\Activity; use Zotlabs\Lib\Config; +use Zotlabs\Lib\Mailer; use Zotlabs\Lib\Zotfinger; use Zotlabs\Lib\Libzot; use Zotlabs\Lib\Queue; @@ -72,21 +73,21 @@ function z_fetch_url($url, $binary = false, $redirects = 0, $opts = array()) { if($ciphers) @curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, $ciphers); - if(x($opts,'filep')) { + if(!empty($opts['filep'])) { @curl_setopt($ch, CURLOPT_FILE, $opts['filep']); @curl_setopt($ch, CURLOPT_HEADER, false); } - if(x($opts,'upload')) + if(!empty($opts['upload'])) @curl_setopt($ch, CURLOPT_UPLOAD, $opts['upload']); - if(x($opts,'infile')) + if(!empty($opts['infile'])) @curl_setopt($ch, CURLOPT_INFILE, $opts['infile']); - if(x($opts,'infilesize')) + if(!empty($opts['infilesize'])) @curl_setopt($ch, CURLOPT_INFILESIZE, $opts['infilesize']); - if(x($opts,'readfunc')) + if(!empty($opts['readfunc'])) @curl_setopt($ch, CURLOPT_READFUNCTION, $opts['readfunc']); // When using the session option and fetching from our own site, @@ -96,7 +97,7 @@ function z_fetch_url($url, $binary = false, $redirects = 0, $opts = array()) { $instance_headers = ((array_key_exists('headers',$opts) && is_array($opts['headers'])) ? $opts['headers'] : []); - if(x($opts,'session')) { + if(!empty($opts['session'])) { if(strpos($url,z_root()) === 0) { $instance_headers[] = 'Cookie: PHPSESSID=' . session_id(); } @@ -105,13 +106,13 @@ function z_fetch_url($url, $binary = false, $redirects = 0, $opts = array()) { @curl_setopt($ch, CURLOPT_HTTPHEADER, $instance_headers); - if(x($opts,'nobody')) + if(!empty($opts['nobody'])) @curl_setopt($ch, CURLOPT_NOBODY, $opts['nobody']); - if(x($opts,'custom')) + if(!empty($opts['custom'])) @curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $opts['custom']); - if(x($opts,'timeout') && intval($opts['timeout'])) { + if(!empty($opts['timeout'])) { @curl_setopt($ch, CURLOPT_TIMEOUT, intval($opts['timeout'])); } else { @@ -119,7 +120,7 @@ function z_fetch_url($url, $binary = false, $redirects = 0, $opts = array()) { @curl_setopt($ch, CURLOPT_TIMEOUT, (($curl_time !== 0) ? $curl_time : 60)); } - if(x($opts,'connecttimeout') && intval($opts['connecttimeout'])) { + if(!empty($opts['connecttimeout'])) { @curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, intval($opts['connecttimeout'])); } else { @@ -127,7 +128,7 @@ function z_fetch_url($url, $binary = false, $redirects = 0, $opts = array()) { @curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (($curl_contime !== 0) ? $curl_contime : 30)); } - if(x($opts,'http_auth')) { + if(!empty($opts['http_auth'])) { // "username" . ':' . "password" @curl_setopt($ch, CURLOPT_USERPWD, $opts['http_auth']); } @@ -135,16 +136,16 @@ function z_fetch_url($url, $binary = false, $redirects = 0, $opts = array()) { if(array_key_exists('http_version',$opts)) @curl_setopt($ch,CURLOPT_HTTP_VERSION,$opts['http_version']); - if(x($opts,'cookiejar')) + if(!empty($opts['cookiejar'])) @curl_setopt($ch, CURLOPT_COOKIEJAR, $opts['cookiejar']); - if(x($opts,'cookiefile')) + if(!empty($opts['cookiefile'])) @curl_setopt($ch, CURLOPT_COOKIEFILE, $opts['cookiefile']); - if(x($opts,'cookie')) + if(!empty($opts['cookie'])) @curl_setopt($ch, CURLOPT_COOKIE, $opts['cookie']); @curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, - ((x($opts,'novalidate') && intval($opts['novalidate'])) ? false : true)); + ((!empty($opts['novalidate'])) ? false : true)); $prx = @Config::Get('system','proxy'); if(strlen($prx)) { @@ -204,7 +205,7 @@ function z_fetch_url($url, $binary = false, $redirects = 0, $opts = array()) { $ret['header'] = $header; $ret['request_target'] = $opts['request_target']; - if(x($opts,'debug')) { + if(!empty($opts['debug'])) { $ret['debug'] = $curl_info; } @@ -432,7 +433,6 @@ function as_return_and_die($obj, $channel = []) { $headers['Content-Type'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ; $headers['Date'] = datetime_convert('UTC','UTC', 'now', 'D, d M Y H:i:s \\G\\M\\T'); $headers['Digest'] = HTTPSig::generate_digest_header($ret); - $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI']; if ($channel) { $h = HTTPSig::create_sig($headers, $channel['channel_prvkey'], channel_url($channel)); @@ -612,7 +612,7 @@ function validate_email(string $addr): bool { $matches = array(); $result = preg_match( - '/^[A-Z0-9._%-]+@([A-Z0-9.-]+\.[A-Z0-9-]{2,})$/i', + '/^[A-Z0-9._%+-]+@([A-Z0-9.-]+\.[A-Z0-9-]{2,})$/i', punify($addr), $matches); @@ -1488,11 +1488,11 @@ function do_delivery($deliveries, $force = false) { $interval = Config::Get('queueworker', 'queue_interval', 500000); - $deliveries_per_process = intval(Config::Get('system','delivery_batch_count')); + $deliveries_per_process = intval(Config::Get('system', 'delivery_batch_count')); - if($deliveries_per_process <= 0) + if($deliveries_per_process <= 0) { $deliveries_per_process = 1; - + } $deliver = []; foreach($deliveries as $d) { @@ -1813,54 +1813,9 @@ function network_to_name($s) { */ function z_mail($params) { - if(! $params['fromEmail']) { - $params['fromEmail'] = Config::Get('system','from_email'); - if(! $params['fromEmail']) - $params['fromEmail'] = 'Administrator' . '@' . App::get_hostname(); - } - if(! $params['fromName']) { - $params['fromName'] = Config::Get('system','from_email_name'); - if(! $params['fromName']) - $params['fromName'] = Zotlabs\Lib\System::get_site_name(); - } - if(! $params['replyTo']) { - $params['replyTo'] = Config::Get('system','reply_address'); - if(! $params['replyTo']) - $params['replyTo'] = 'noreply' . '@' . App::get_hostname(); - } - - $params['sent'] = false; - $params['result'] = false; - - /** - * @hooks email_send - * * \e params @see z_mail() - */ - call_hooks('email_send', $params); - - if($params['sent']) { - logger('notification: z_mail returns ' . (($params['result']) ? 'success' : 'failure'), LOGGER_DEBUG); - return $params['result']; - } - - $fromName = email_header_encode(html_entity_decode($params['fromName'],ENT_QUOTES,'UTF-8'),'UTF-8'); - $messageSubject = email_header_encode(html_entity_decode($params['messageSubject'],ENT_QUOTES,'UTF-8'),'UTF-8'); - - $messageHeader = - $params['additionalMailHeader'] . - "From: $fromName <{$params['fromEmail']}>" . PHP_EOL . - "Reply-To: $fromName <{$params['replyTo']}>" . PHP_EOL . - "Content-Type: text/plain; charset=UTF-8"; - - // send the message - $res = mail( - $params['toEmail'], // send to address - $messageSubject, // subject - $params['textVersion'], - $messageHeader // message headers - ); - logger('notification: z_mail returns ' . (($res) ? 'success' : 'failure'), LOGGER_DEBUG); - return $res; + // Delegate the call to the Mailer class. + $mailer = new Mailer($params); + return $mailer->deliver(); } @@ -2145,21 +2100,59 @@ function get_request_string($url) { /** - * Builds a url from the result of `parse_url`. + * Reconstructs a URL from its parsed components. * - * @param array $parsed_url An associative array as produced by `parse_url`. + * This function takes a parsed URL as an associative array and reconstructs + * the URL based on the specified components (scheme, host, port, user, pass, path, query, fragment). + * You can specify which components should be included in the final URL by passing the optional + * `$parts` array. The function will return the complete URL string formed by combining + * only the parts that exist in both the parsed URL and the `$parts` array. * - * @return string The reassembled URL as a string. + * @param array $parsed_url The parsed URL components as an associative array. + * The array can include keys like 'scheme', 'host', 'port', 'user', 'pass', + * 'path', 'query', 'fragment'. + * + * @param array $parts An optional array that specifies which components of the URL + * should be included in the final string. Defaults to: + * ['scheme', 'host', 'port', 'user', 'pass', 'path', 'query', 'fragment']. + * If any of the components are not required, they can be omitted from the array. + * + * @return string The reconstructed URL as a string. */ -function unparse_url(array $parsed_url): string { - $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; - $host = isset($parsed_url['host']) ? $parsed_url['host'] : ''; - $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; - $user = isset($parsed_url['user']) ? $parsed_url['user'] : ''; - $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; - $pass = ($user || $pass) ? "$pass@" : ''; - $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; - $query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; - $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; - return $scheme . $user . $pass . $host . $port . $path . $query . $fragment; +function unparse_url(array $parsed_url, array $parts = ['scheme', 'host', 'port', 'user', 'pass', 'path', 'query', 'fragment']): string { + $url_parts = []; + + if (in_array('scheme', $parts) && array_key_exists('scheme', $parsed_url)) { + $url_parts[] = $parsed_url['scheme'] . '://'; + } + + if (in_array('user', $parts) && array_key_exists('user', $parsed_url)) { + $url_parts[] = $parsed_url['user']; + if (in_array('pass', $parts) && array_key_exists('pass', $parsed_url)) { + $url_parts[] = ':' . $parsed_url['pass']; + } + $url_parts[] = '@'; + } + + if (in_array('host', $parts) && array_key_exists('host', $parsed_url)) { + $url_parts[] = $parsed_url['host']; + } + + if (in_array('port', $parts) && array_key_exists('port', $parsed_url)) { + $url_parts[] = ':' . $parsed_url['port']; + } + + if (in_array('path', $parts) && array_key_exists('path', $parsed_url)) { + $url_parts[] = $parsed_url['path']; + } + + if (in_array('query', $parts) && array_key_exists('query', $parsed_url)) { + $url_parts[] = '?' . $parsed_url['query']; + } + + if (in_array('fragment', $parts) && array_key_exists('fragment', $parsed_url)) { + $url_parts[] = '#' . $parsed_url['fragment']; + } + + return implode('', $url_parts); } diff --git a/include/observer.php b/include/observer.php new file mode 100644 index 000000000..4483e1d8b --- /dev/null +++ b/include/observer.php @@ -0,0 +1,68 @@ +<?php +/** + * Helper functions for getting info about the observer. + * + * SPDX-FileCopyrightText: 2025 The Hubzilla Community + * SPDX-FileContributor: Harald Eilertsen <haraldei@anduin.net> + * + * SPDX-License-Identifier: MIT + * + * The _observer_ in Hubzilla is the channel visiting the site in the current + * session. This could be a local channel, or a remote channel logged in via + * OpenWebAuth. + * + * If the observer is not set, or empty, this indicates an unauthenticated + * visitor, which may mean a visitor from another site that don't support, or + * has not enabled OpenWebAuth. + */ + +/** + * Get the unique hash identifying the current observer. + * + * Observer can be a local or remote channel. + * + * @return string Unique hash of observer, otherwise empty string if no + * observer + */ +function get_observer_hash() { + $observer = App::get_observer(); + if (is_array($observer)) { + return $observer['xchan_hash']; + } + + return ''; +} + +/** + * Get the guid of the current observer. + * + * Observer can be a local or remote channel. + * + * @return string The GUID of the observer, otherwise empty string if no + * observer + */ +function get_observer_guid() { + $observer = App::get_observer(); + if (is_array($observer)) { + return $observer['xchan_guid']; + } + + return ''; +} + +/** + * Get the name of the current observer. + * + * Observer can be a local or remote channel. + * + * @return string The name of the observer, otherwise empty string if no + * observer + */ +function get_observer_name() { + $observer = App::get_observer(); + if (is_array($observer)) { + return $observer['xchan_name']; + } + + return ''; +} diff --git a/include/oembed.php b/include/oembed.php index f52f73225..840164663 100644 --- a/include/oembed.php +++ b/include/oembed.php @@ -143,6 +143,10 @@ function oembed_fetch_url($embedurl){ $furl = ((local_channel() && $zrl) ? zid($embedurl) : $embedurl); + if (empty($furl)) { + return; + } + if($action !== 'block' && (! Config::Get('system','oembed_cache_disable'))) { $txt = Cache::get('[' . App::$videowidth . '] ' . $furl); } diff --git a/include/permissions.php b/include/permissions.php index be6fc8594..29d242537 100644 --- a/include/permissions.php +++ b/include/permissions.php @@ -408,7 +408,7 @@ function get_all_api_perms($uid,$api) { $arr = array( 'channel_id' => $uid, - 'observer_hash' => $observer_xchan, + 'observer_hash' => null, //$observer_xchan, 'permissions' => $ret); call_hooks('get_all_api_perms',$arr); @@ -422,7 +422,7 @@ function api_perm_is_allowed($uid,$api,$permission) { $arr = array( 'channel_id' => $uid, - 'observer_hash' => $observer_xchan, + 'observer_hash' => null, //$observer_xchan, 'permission' => $permission, 'result' => false ); diff --git a/include/photo/photo_driver.php b/include/photo/photo_driver.php index 66a5d19f9..88b9d1d62 100644 --- a/include/photo/photo_driver.php +++ b/include/photo/photo_driver.php @@ -65,114 +65,95 @@ function photo_factory($data, $type = null) { * * @param string $filename * Image filename - * @param string $data (optional) + * @param array $data (optional) * Data array fetched from cURL with z_fetch_url * @return null|string Guessed mimetype */ -function guess_image_type($filename, $data = '') { - - if($data) - $headers = (is_array($data) ? $data['header'] : $data); - - // logger('Photo: guess_image_type: '.$filename . ($headers?' from curl headers':''), LOGGER_DEBUG); - - $type = null; - $m = null; - $headers = ''; - +function guess_image_type($filename, $data = []) { $ph = photo_factory(''); $types = $ph->supportedTypes(); - if($headers) { - $hdrs = []; - $h = explode("\n", $headers); - foreach ($h as $l) { - if (strpos($l, ':') === false) { - continue; - } + logger('filename: ' . print_r($filename, true), LOGGER_DEBUG); - list($k, $v) = array_map('trim', explode(':', trim($l), 2)); - $hdrs[strtolower($k)] = $v; + // Try Fileinfo from raw data + if (class_exists('finfo') && !empty($data['body'])) { + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mime = $finfo->buffer($data['body']); + if ($mime && array_key_exists($mime, $types)) { + logger('finfo mime type: ' . print_r($mime, true), LOGGER_DEBUG); + return $mime; } - logger('Curl headers: ' .var_export($hdrs, true), LOGGER_DEBUG); - if(array_key_exists('content-type', $hdrs) && array_key_exists($hdrs['content-type'], $types)) - $type = $hdrs['content-type']; } - if(is_null($type)){ - $ignore_imagick = Config::Get('system', 'ignore_imagick'); - // Guessing from extension? Isn't that... dangerous? - if(class_exists('Imagick') && ! $ignore_imagick) { - $v = Imagick::getVersion(); - preg_match('/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $v['versionString'], $m); - if(version_compare($m[1], '6.6.7') >= 0) { - /** - * Well, this not much better, - * but at least it comes from the data inside the image, - * we won't be tricked by a manipulated extension - */ - $body = false; - if (strpos($filename, 'http') === false && file_exists($filename) && is_readable($filename)) - $body == file_get_contents($filename); - elseif (is_array($data) && array_key_exists('body', $data)) - $body = $data['body']; - if ($body) { - $image = new Imagick(); - - try{ - $image->readImageBlob($body); - } catch (\Exception $e) { - logger('Imagick readImageBlob() exception:' . print_r($e, true)); - return $type; - } - - $r = $image->identifyImage(); - if ($r && is_array($r) && array_key_exists($r['mimetype'], $types)) - $type = $r['mimetype']; - } - } - else { - // earlier imagick versions have issues with scaling png's - // don't log this because it will just fill the logfile. - // leave this note here so those who are looking for why - // we aren't using imagick can find it + // Try exif_imagetype + image_type_to_mime_type if file exists locally + if (function_exists('exif_imagetype') && is_file($filename) && is_readable($filename)) { + $image_type = @exif_imagetype($filename); + if ($image_type !== false) { + $mime = image_type_to_mime_type($image_type); + if ($mime && array_key_exists($mime, $types)) { + logger('exif_imagetype mime type: ' . print_r($mime, true), LOGGER_DEBUG); + return $mime; } } + } - if(is_null($type)) { - $ext = pathinfo($filename, PATHINFO_EXTENSION); - foreach($types as $m => $e) { - if($ext === $e) { - $type = $m; + // Try getimagesize for URLs + if (filter_var($filename, FILTER_VALIDATE_URL)) { + $size = @getimagesize($filename); + if (isset($size['mime']) && array_key_exists($size['mime'], $types)) { + logger('getimagesize mime type: ' . print_r($size['mime'], true), LOGGER_DEBUG); + return $size['mime']; + } + } + + // Try Imagick if available and not disabled + $ignore_imagick = Config::Get('system', 'ignore_imagick'); + if (class_exists('Imagick') && !$ignore_imagick) { + $v = Imagick::getVersion(); + if (preg_match('/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $v['versionString'], $m) && version_compare($m[1], '6.6.7') >= 0) { + $body = false; + if (is_file($filename) && is_readable($filename)) { + $body = file_get_contents($filename); + } elseif (!empty($data['body'])) { + $body = $data['body']; + } + if ($body) { + $image = new Imagick(); + try { + $image->readImageBlob($body); + $r = $image->identifyImage(); + if (isset($r['mimetype']) && array_key_exists($r['mimetype'], $types)) { + logger('imagick mime type: ' . print_r($r['mimetype'], true), LOGGER_DEBUG); + return $r['mimetype']; + } + } catch (\Exception $e) { + logger('Imagick readImageBlob() exception:' . print_r($e, true)); } } } + } - if(is_null($type) && strpos($filename, 'http') === 0) { - $size = getimagesize($filename); - if ($size && array_key_exists($size['mime'], $types)) - $type = $size['mime']; + // Try Content-Type header + if (!empty($data['header'])) { + $hdrs = []; + foreach (explode("\n", $data['header']) as $l) { + if (strpos($l, ':') !== false) { + list($k, $v) = array_map('trim', explode(':', trim($l), 2)); + $hdrs[strtolower($k)] = $v; + } } - - if(is_null($type)) { - if(strpos(strtolower($filename),'jpg') !== false) - $type = 'image/jpeg'; - elseif(strpos(strtolower($filename),'jpeg') !== false) - $type = 'image/jpeg'; - elseif(strpos(strtolower($filename),'gif') !== false) - $type = 'image/gif'; - elseif(strpos(strtolower($filename),'png') !== false) - $type = 'image/png'; - elseif(strpos(strtolower($filename),'webp') !== false) - $type = 'image/webp'; + if (isset($hdrs['content-type']) && array_key_exists($hdrs['content-type'], $types)) { + logger('headers mime type: ' . print_r($hdrs['content-type'], true), LOGGER_DEBUG); + return $hdrs['content-type']; } - } - logger('Photo: guess_image_type: filename = ' . $filename . ' type = ' . $type, LOGGER_DEBUG); - return $type; + logger('failed to guess image type', LOGGER_DEBUG); + + return null; } + /** * @brief Delete thing photo from database. * diff --git a/include/photos.php b/include/photos.php index 85c97d1fd..a9f92e103 100644 --- a/include/photos.php +++ b/include/photos.php @@ -7,6 +7,7 @@ use Zotlabs\Access\PermissionLimits; use Zotlabs\Lib\Activity; use Zotlabs\Lib\Config; +use Zotlabs\Daemon\Master; require_once('include/permissions.php'); require_once('include/items.php'); @@ -405,7 +406,7 @@ function photo_upload($channel, $observer, $args) { } } - $attribution = (($visitor) ? $visitor : $channel['xchan_url']); + $attribution = (($visitor) ? $visitor : channel_url($channel)); //// Create item object $object = [ @@ -438,13 +439,13 @@ function photo_upload($channel, $observer, $args) { else { $object['to'] = Activity::map_acl(array_merge($ac, ['item_private' => 1 - intval($public)])); } - +/* $target = [ 'type' => 'orderedCollection', 'name' => ((strlen($album)) ? $album : '/'), 'id' => z_root() . '/album/' . $channel['channel_address'] . ((isset($args['directory']['hash'])) ? '/' . $args['directory']['hash'] : EMPTY_STR) ]; - +*/ // Create item container if (isset($args['item'])) { foreach ($args['item'] as $i) { @@ -453,6 +454,11 @@ function photo_upload($channel, $observer, $args) { $force = false; if ($item['mid'] === $item['parent_mid']) { + $target = [ + 'id' => str_replace('/item/', '/conversation/', $item['mid']), + 'type' => 'Collection', + 'attributedTo' => $attribution, + ]; $item['body'] = $summary; $item['mimetype'] = 'text/bbcode'; @@ -460,10 +466,10 @@ function photo_upload($channel, $observer, $args) { $object['id'] = $item['mid']; $object['diaspora:guid'] = $item['uuid']; - $item['obj'] = json_encode($object); + $item['obj'] = $object; - $item['tgt_type'] = 'orderedCollection'; - $item['target'] = json_encode($target); + $item['tgt_type'] = 'Collection'; + $item['target'] = $target; if ($post_tags) { $arr['term'] = $post_tags; } @@ -477,15 +483,23 @@ function photo_upload($channel, $observer, $args) { if (($item['edited'] > $r[0]['edited']) || $force) { $item['id'] = $r[0]['id']; $item['uid'] = $channel['channel_id']; - item_store_update($item, false, $deliver); + $result = item_store_update($item, deliver: $deliver); continue; } } else { $item['aid'] = $channel['channel_account_id']; $item['uid'] = $channel['channel_id']; - item_store($item, false, $deliver); + $result = item_store($item, deliver: $deliver); } + + if ($result['success'] && $visible && $deliver) { + Master::Summon(['Notifier', 'wall-new', $result['item_id']]); + if (!empty($result['approval_id'])) { + Master::Summon(['Notifier', 'wall-new', $result['approval_id']]); + } + } + } } else { @@ -496,6 +510,12 @@ function photo_upload($channel, $observer, $args) { $object['id'] = $mid; $object['diaspora:guid'] = $uuid; + $target = [ + 'id' => z_root() . '/conversation/' . $uuid, + 'type' => 'Collection', + 'attributedTo' => $attribution, + ]; + $arr = [ 'aid' => $account_id, 'uid' => $channel_id, @@ -514,9 +534,9 @@ function photo_upload($channel, $observer, $args) { 'deny_gid' => $ac['deny_gid'], 'verb' => 'Create', 'obj_type' => 'Image', - 'obj' => json_encode($object), - 'tgt_type' => 'orderedCollection', - 'target' => json_encode($target), + 'obj' => $object, + 'tgt_type' => 'Collection', + 'target' => $target, 'item_wall' => $visible, 'item_origin' => 1, 'item_thread_top' => 1, @@ -541,21 +561,26 @@ function photo_upload($channel, $observer, $args) { // linked item from leaking into the feed when somebody has a channel with read_stream restrictions. $arr['public_policy'] = map_scope(PermissionLimits::Get($channel['channel_id'], 'view_stream'), true); - if ($arr['public_policy']) + + if ($arr['public_policy']) { $arr['item_private'] = 1; + } - $result = item_store($arr, false, $deliver); - $item_id = $result['item_id']; + $result = item_store($arr, deliver: $deliver); - if ($visible && $deliver) - Zotlabs\Daemon\Master::Summon(['Notifier', 'wall-new', $item_id]); + if ($result['success'] && $visible && $deliver) { + Master::Summon(['Notifier', 'wall-new', $result['item_id']]); + if (!empty($result['approval_id'])) { + Master::Summon(['Notifier', 'wall-new', $result['approval_id']]); + } + } } $ret['success'] = true; $ret['item'] = $arr; $ret['body'] = $obj_body; $ret['resource_id'] = $photo_hash; - $ret['photoitem_id'] = $item_id; + $ret['photoitem_id'] = $result['item_id']; /** * @hooks photo_upload_end @@ -911,7 +936,7 @@ function photos_create_item($channel, $creator_hash, $photo, $visible = false) { . '[zmg]' . z_root() . '/photo/' . $photo['resource_id'] . '-' . $photo['imgscale'] . '[/zmg]' . '[/zrl]'; - $result = item_store($arr); + $result = item_store($arr, deliver: false, addAndSync: true); $item_id = $result['item_id']; return $item_id; diff --git a/include/plugin.php b/include/plugin.php index 62b643c3e..b5f9959b9 100644 --- a/include/plugin.php +++ b/include/plugin.php @@ -987,12 +987,13 @@ function format_css_if_exists($source) { } } else { // It's a file from the theme - $path = theme_include($script); + $theme_include = theme_include($script); + $path = (($theme_include) ? '/' . $theme_include : ''); } if($path) { $qstring = ((parse_url($path, PHP_URL_QUERY)) ? '&' : '?') . 'v=' . STD_VERSION; - return '<link rel="stylesheet" href="' . $path_prefix . '/' . $path . $qstring . '" type="text/css" media="' . $source[1] . '">' . "\r\n"; + return '<link rel="stylesheet" href="' . $path_prefix . $path . $qstring . '" type="text/css" media="' . $source[1] . '">' . "\r\n"; } } @@ -1059,11 +1060,12 @@ function format_js_if_exists($source) { } else { // It's a file from the theme - $path = theme_include($source); + $theme_include = theme_include($source); + $path = (($theme_include) ? '/' . $theme_include : ''); } if($path) { $qstring = ((parse_url($path, PHP_URL_QUERY)) ? '&' : '?') . 'v=' . STD_VERSION; - return '<script src="' . $path_prefix . '/' . $path . $qstring . '" ></script>' . "\r\n" ; + return '<script src="' . $path_prefix . $path . $qstring . '"></script>' . "\r\n" ; } } diff --git a/include/security.php b/include/security.php index 545788bcd..32ca4f268 100644 --- a/include/security.php +++ b/include/security.php @@ -22,7 +22,7 @@ function authenticate_success($user_record, $channel = null, $login_initial = fa $lastlog_updated = false; $uid_to_load = null; - if (x($user_record, 'account_id')) { + if (!empty($user_record['account_id'])) { App::$account = $user_record; $_SESSION['account_id'] = $user_record['account_id']; $_SESSION['authenticated'] = 1; @@ -31,7 +31,7 @@ function authenticate_success($user_record, $channel = null, $login_initial = fa $uid_to_load = $channel['channel_id']; if (!$uid_to_load) { - $uid_to_load = (((x($_SESSION, 'uid')) && (intval($_SESSION['uid']))) + $uid_to_load = ((!empty($_SESSION['uid'])) ? intval($_SESSION['uid']) : intval(App::$account['account_default_channel']) ); @@ -60,12 +60,12 @@ function authenticate_success($user_record, $channel = null, $login_initial = fa // might want to log success here } - if ($return || x($_SESSION, 'workflow')) { + if ($return || isset($_SESSION['workflow'])) { unset($_SESSION['workflow']); return; } - if ((App::$module !== 'home') && x($_SESSION, 'login_return_url') && strlen($_SESSION['login_return_url'])) { + if (App::$module !== 'home' && !empty($_SESSION['login_return_url'])) { $return_url = $_SESSION['login_return_url']; // don't let members get redirected to a raw ajax page update - this can happen @@ -321,6 +321,7 @@ function change_channel($change_channel) { function permissions_sql($owner_id, $remote_observer = null, $table = '', $token = EMPTY_STR) { $local_channel = local_channel(); + $observer = $remote_observer ?? get_observer_hash(); /** * Construct permissions @@ -344,15 +345,22 @@ function permissions_sql($owner_id, $remote_observer = null, $table = '', $token if (($local_channel) && ($local_channel == $owner_id)) { return EMPTY_STR; } - /** - * Authenticated visitor. - */ - else { - $observer = ((!is_null($remote_observer)) ? $remote_observer : get_observer_hash()); + /* + * OCAP token access + */ + + if ($token) { + $sql = " AND ( {$table}allow_cid like '" . protect_sprintf('%<token:' . $token . '>%') . + "' OR ( {$table}allow_cid = '' AND {$table}allow_gid = '' AND {$table}deny_cid = '' AND {$table}deny_gid = '' ) )"; + } - if ($observer) { + /** + * Authenticated visitor. + */ + + elseif ($observer) { $sec = get_security_ids($owner_id, $observer); @@ -400,16 +408,6 @@ function permissions_sql($owner_id, $remote_observer = null, $table = '', $token dbesc($gs) ); } - - /* - * OCAP token access - */ - - elseif ($token) { - $sql = " AND ( {$table}allow_cid like '" . protect_sprintf('%<token:' . $token . '>%') . - "' OR ( {$table}allow_cid = '' AND {$table}allow_gid = '' AND {$table}deny_cid = '' AND {$table}deny_gid = '' ) )"; - } - } return $sql; @@ -434,7 +432,7 @@ function item_permissions_sql($owner_id, $remote_observer = null) { * default permissions - anonymous user */ - $sql = " AND item_private = 0 "; + $sql = " AND item.item_private = 0 "; /** * Profile owner - everything is visible @@ -494,10 +492,10 @@ function item_permissions_sql($owner_id, $remote_observer = null) { $regexop = db_getfunc('REGEXP'); $sql = sprintf( - " AND ( author_xchan = '%s' OR owner_xchan = '%s' OR - (( NOT (deny_cid $regexop '%s' OR deny_gid $regexop '%s') - AND ( allow_cid $regexop '%s' OR allow_gid $regexop '%s' OR ( allow_cid = '' AND allow_gid = '' AND item_private = 0 )) - ))) + " AND ( item.author_xchan = '%s' OR item.owner_xchan = '%s' OR + (( NOT (item.deny_cid $regexop '%s' OR item.deny_gid $regexop '%s') + AND ( item.allow_cid $regexop '%s' OR item.allow_gid $regexop '%s' OR ( item.allow_cid = '' AND item.allow_gid = '' AND item.item_private = 0 )) + )) OR ( item.item_private = 1 $scope )) ", dbesc($observer), dbesc($observer), @@ -520,11 +518,11 @@ function item_permissions_sql($owner_id, $remote_observer = null) { function scopes_sql($uid, $observer) { - $str = " and ( public_policy = 'authenticated' "; + $str = " and ( item.public_policy = 'authenticated' "; if (!is_foreigner($observer)) - $str .= " or public_policy = 'network: red' "; + $str .= " or item.public_policy = 'network: red' "; if (local_channel()) - $str .= " or public_policy = 'site: " . App::get_hostname() . "' "; + $str .= " or item.public_policy = 'site: " . App::get_hostname() . "' "; $ab = q("select * from abook where abook_xchan = '%s' and abook_channel = %d limit 1", dbesc($observer), @@ -533,8 +531,8 @@ function scopes_sql($uid, $observer) { if (!$ab) return $str . " ) "; if ($ab[0]['abook_pending']) - $str .= " or public_policy = 'any connections' "; - $str .= " or public_policy = 'contacts' ) "; + $str .= " or item.public_policy = 'any connections' "; + $str .= " or item.public_policy = 'contacts' ) "; return $str; } @@ -609,14 +607,14 @@ function public_permissions_sql($observer_hash) { function get_form_security_token($typename = '') { $timestamp = time(); - $guid = App::$observer['xchan_guid'] ?? ''; + $guid = get_observer_guid(); $sec_hash = hash('whirlpool', $guid . ((local_channel()) ? App::$channel['channel_prvkey'] : '') . session_id() . $timestamp . $typename); return $timestamp . '.' . $sec_hash; } function check_form_security_token($typename = '', $formname = 'form_security_token') { - if (!x($_REQUEST, $formname)) return false; + if (empty($_REQUEST[$formname])) return false; $hash = $_REQUEST[$formname]; $max_livetime = 10800; // 3 hours @@ -625,7 +623,7 @@ function check_form_security_token($typename = '', $formname = 'form_security_to if (time() > (IntVal($x[0]) + $max_livetime)) return false; - $sec_hash = hash('whirlpool', App::$observer['xchan_guid'] . ((local_channel()) ? App::$channel['channel_prvkey'] : '') . session_id() . $x[0] . $typename); + $sec_hash = hash('whirlpool', get_observer_guid() . ((local_channel()) ? App::$channel['channel_prvkey'] : '') . session_id() . $x[0] . $typename); return ($sec_hash == $x[1]); } @@ -637,7 +635,7 @@ function check_form_security_std_err_msg() { function check_form_security_token_redirectOnErr($err_redirect, $typename = '', $formname = 'form_security_token') { if (!check_form_security_token($typename, $formname)) { - logger('check_form_security_token failed: user ' . App::$observer['xchan_name'] . ' - form element ' . $typename); + logger('check_form_security_token failed: user ' . get_observer_name() . ' - form element ' . $typename); logger('check_form_security_token failed: _REQUEST data: ' . print_r($_REQUEST, true), LOGGER_DATA); notice(check_form_security_std_err_msg()); goaway(z_root() . $err_redirect); @@ -646,7 +644,7 @@ function check_form_security_token_redirectOnErr($err_redirect, $typename = '', function check_form_security_token_ForbiddenOnErr($typename = '', $formname = 'form_security_token') { if (!check_form_security_token($typename, $formname)) { - logger('check_form_security_token failed: user ' . App::$observer['xchan_name'] . ' - form element ' . $typename); + logger('check_form_security_token failed: user ' . get_observer_name() . ' - form element ' . $typename); logger('check_form_security_token failed: _REQUEST data: ' . print_r($_REQUEST, true), LOGGER_DATA); header('HTTP/1.1 403 Forbidden'); killme(); @@ -708,56 +706,61 @@ function get_security_ids($channel_id, $ob_hash) { 'allow_gid' => [] ]; - if ($channel_id) { - $ch = q("select channel_hash from channel where channel_id = %d", - intval($channel_id) - ); - if ($ch) { - $ret['channel_id'][] = $ch[0]['channel_hash']; - } - } - - $groups = []; - - $x = q("select * from xchan where xchan_hash = '%s'", + $x = q("select xchan_hash from xchan where xchan_hash = '%s'", dbesc($ob_hash) ); - if ($x) { + if (!$x) { + return $ret; + } - // include xchans for all zot-like networks + $ret['allow_cid'][] = $x[0]['xchan_hash']; - $xchans = q("select xchan_hash from xchan where xchan_hash = '%s' OR ( xchan_guid = '%s' AND xchan_pubkey = '%s' ) ", - dbesc($ob_hash), - dbesc($x[0]['xchan_guid']), - dbesc($x[0]['xchan_pubkey']) - ); + if (!$channel_id) { + return $ret; + } + + $ch = q("select channel_hash from channel where channel_id = %d", + intval($channel_id) + ); + if ($ch) { + $ret['channel_id'][] = $ch[0]['channel_hash']; + } - if ($xchans) { - $ret['allow_cid'] = ids_to_array($xchans, 'xchan_hash'); - $hashes = ids_to_querystr($xchans, 'xchan_hash', true); + $groups = []; - // private profiles are treated as a virtual group + // private profiles are treated as a virtual group - $r = q("SELECT abook_profile from abook where abook_xchan in ( " . protect_sprintf($hashes) . " ) and abook_profile != '' "); - if ($r) { - foreach ($r as $rv) { - $groups[] = 'vp.' . $rv['abook_profile']; - } + $r = q("SELECT abook_profile from abook where abook_channel = %d and abook_xchan = '%s' and abook_profile != ''", + intval($channel_id), + dbesc(protect_sprintf($x[0]['xchan_hash'])) + ); + + if ($r) { + foreach ($r as $rv) { + if (!in_array('vp.' . $rv['abook_profile'], $groups)) { + $groups[] = 'vp.' . $rv['abook_profile']; } + } + } - // physical groups this identity is a member of + // physical groups this identity is a member of - $r = q("SELECT hash FROM pgrp left join pgrp_member on pgrp.id = pgrp_member.gid WHERE xchan in ( " . protect_sprintf($hashes) . " ) "); - if ($r) { - foreach ($r as $rv) { - $groups[] = $rv['hash']; - } + $r = q("SELECT hash FROM pgrp left join pgrp_member on pgrp.id = pgrp_member.gid WHERE pgrp.uid = %d and pgrp_member.xchan = '%s'", + intval($channel_id), + dbesc(protect_sprintf($x[0]['xchan_hash'])) + ); + + if ($r) { + foreach ($r as $rv) { + if (!in_array($rv['hash'], $groups)) { + $groups[] = $rv['hash']; } - $ret['allow_gid'] = $groups; } } + $ret['allow_gid'] = $groups; + return $ret; } diff --git a/include/socgraph.php b/include/socgraph.php index 336c1c0c3..2da0040b0 100644 --- a/include/socgraph.php +++ b/include/socgraph.php @@ -364,8 +364,8 @@ function poco() { elseif(argv(3) === '@self') $justme = true; } - if(argc() > 4 && intval(argv(4)) && $justme == false) - $cid = intval(argv(4)); + + $cid = ((argc() > 4 && intval(argv(4)) && $justme == false) ? intval(argv(4)) : null); if(! $system_mode) { @@ -413,11 +413,8 @@ function poco() { else $totalResults = 0; - $startIndex = intval($_GET['startIndex']); - if(! $startIndex) - $startIndex = 0; - - $itemsPerPage = ((x($_GET,'count') && intval($_GET['count'])) ? intval($_GET['count']) : $totalResults); + $startIndex = $_GET['startIndex'] ?? 0; + $itemsPerPage = $_GET['count'] ?? $totalResults; if($system_mode) { $r = q("SELECT abook.*, xchan.* from abook left join xchan on abook_xchan = xchan_hash where abook_self = 1 diff --git a/include/text.php b/include/text.php index b03e2d1a9..9ac6efdc2 100644 --- a/include/text.php +++ b/include/text.php @@ -1145,7 +1145,7 @@ function chanlink_cid($d) { function magiclink_url($observer,$myaddr,$url) { return (($observer) - ? z_root() . '/magic?f=&owa=1&bdest=' . bin2hex($url) . '&addr=' . $myaddr + ? z_root() . '/magic?owa=1&bdest=' . bin2hex($url) . '&addr=' . $myaddr : $url ); } @@ -1272,24 +1272,38 @@ function sslify($s) { function get_emojis() { $emojis = [ // Reactions (emojitwo emojis) - 'astonished_face' => ['shortname' => ':astonished_face:', 'filepath' => 'images/emoji/astonished_face.png'], - 'bottle_with_popping_cork' => ['shortname' => ':bottle_with_popping_cork:', 'filepath' => 'images/emoji/bottle_with_popping_cork.png'], - 'clapping_hands' => ['shortname' => ':clapping_hands:', 'filepath' => 'images/emoji/clapping_hands.png'], - 'disappointed_face' => ['shortname' => ':disappointed_face:', 'filepath' => 'images/emoji/disappointed_face.png'], - 'grinning_face' => ['shortname' => ':grinning_face:', 'filepath' => 'images/emoji/grinning_face.png'], - 'kiss_mark' => ['shortname' => ':kiss_mark:', 'filepath' => 'images/emoji/kiss_mark.png'], - 'red_heart' => ['shortname' => ':red_heart:', 'filepath' => 'images/emoji/red_heart.png'], - 'sleeping_face' => ['shortname' => ':sleeping_face:', 'filepath' => 'images/emoji/sleeping_face.png'], - 'slightly_smiling_face' => ['shortname' => ':slightly_smiling_face:', 'filepath' => 'images/emoji/slightly_smiling_face.png'], - 'smiling_face_with_halo' => ['shortname' => ':smiling_face_with_halo:', 'filepath' => 'images/emoji/smiling_face_with_halo.png'], - 'smiling_face_with_horns' => ['shortname' => ':smiling_face_with_horns:', 'filepath' => 'images/emoji/smiling_face_with_horns.png'], - 'winking_face_with_tongue' => ['shortname' => ':winking_face_with_tongue:', 'filepath' => 'images/emoji/winking_face_with_tongue.png'], - - 'facepalm' => ['shortname' => ':facepalm:', 'filepath' => 'images/emoticons/smiley-facepalm.gif'] + 'astonished_face' => ['shortname' => ':astonished_face:', 'filepath' => 'images/emoji/emojitwo/astonished_face.png'], + 'bottle_with_popping_cork' => ['shortname' => ':bottle_with_popping_cork:', 'filepath' => 'images/emoji/emojitwo/bottle_with_popping_cork.png'], + 'clapping_hands' => ['shortname' => ':clapping_hands:', 'filepath' => 'images/emoji/emojitwo/clapping_hands.png'], + 'disappointed_face' => ['shortname' => ':disappointed_face:', 'filepath' => 'images/emoji/emojitwo/disappointed_face.png'], + 'grinning_face' => ['shortname' => ':grinning_face:', 'filepath' => 'images/emoji/emojitwo/grinning_face.png'], + 'kiss_mark' => ['shortname' => ':kiss_mark:', 'filepath' => 'images/emoji/emojitwo/kiss_mark.png'], + 'red_heart' => ['shortname' => ':red_heart:', 'filepath' => 'images/emoji/emojitwo/red_heart.png'], + 'sleeping_face' => ['shortname' => ':sleeping_face:', 'filepath' => 'images/emoji/emojitwo/sleeping_face.png'], + 'slightly_smiling_face' => ['shortname' => ':slightly_smiling_face:', 'filepath' => 'images/emoji/emojitwo/slightly_smiling_face.png'], + 'smiling_face_with_halo' => ['shortname' => ':smiling_face_with_halo:', 'filepath' => 'images/emoji/emojitwo/smiling_face_with_halo.png'], + 'smiling_face_with_horns' => ['shortname' => ':smiling_face_with_horns:', 'filepath' => 'images/emoji/emojitwo/smiling_face_with_horns.png'], + 'winking_face_with_tongue' => ['shortname' => ':winking_face_with_tongue:', 'filepath' => 'images/emoji/emojitwo/winking_face_with_tongue.png'], + + // Hubzilla custom + 'facepalm' => ['shortname' => ':facepalm:', 'filepath' => 'images/emoji/hubzilla/smiley-facepalm.gif'], + 'hubzilla' => ['shortname' => ':hubzilla:', 'filepath' => 'images/emoji/hubzilla/hubzilla.png'] ]; + // Provided by addon call_hooks('get_emojis', $emojis); + // Custom site emojis + $custom_json_path = 'images/emoji/custom/custom_emojis.json'; + if (file_exists($custom_json_path)) { + $custom_json = file_get_contents($custom_json_path); + $custom_arr = json_decode($custom_json, true); + + if ($custom_arr) { + $emojis = array_merge($emojis, $custom_arr); + } + } + return $emojis; } @@ -1406,14 +1420,12 @@ function list_smilies($default_only = false) { * @param boolean $sample (optional) default false * @return string */ -function smilies($s, $sample = false) { - +function smilies($s, $sample = false, $terms = []) { if(intval(Config::Get('system', 'no_smilies')) || (local_channel() && intval(get_pconfig(local_channel(), 'system', 'no_smilies')))) return $s; - - $s = preg_replace_callback('{<(pre|code)>.*?</\1>}ism', 'smile_shield', $s); + $s = preg_replace_callback('/<(pre|code)\b[^>]*>.*?<\/(pre|code)>/ism', 'smile_shield', $s); $s = preg_replace_callback('/<[a-z]+ .*?>/ism', 'smile_shield', $s); if (preg_match_all('/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/', $s, $match)) { @@ -1421,13 +1433,23 @@ function smilies($s, $sample = false) { $emojis = get_emojis(); foreach ($match[0] as $mtch) { $name = trim($mtch, ':'); + $emoji = $emojis[$name] ?? []; + + if (!$emoji && !empty($terms)) { + foreach($terms as $term) { + // some platforms provide the term without : + $term['term'] = ':' . trim($term['term'], ':') . ':'; + if (intval($term['ttype']) === TERM_EMOJI && $term['term'] === $mtch) { + $emoji['filepath'] = $term['imgurl']; + $emoji['shortname'] = $term['term']; + } + } + } - if (!isset($emojis[$name])) { + if (!$emoji || empty($emoji['filepath'])) { continue; } - $emoji = $emojis[$name]; - $class = 'emoji'; if (is_solo_string($mtch, $s)) { $class .= ' single-emoji'; @@ -1579,18 +1601,16 @@ function theme_attachments(&$item) { $title = t('Size') . ' ' . (isset($r['length']) ? userReadableSize($r['length']) : t('unknown')); - $revision = $r['revision'] ?? ''; - require_once('include/channel.php'); if (isset($r['href'])) { if(is_foreigner($item['author_xchan'])) $url = $r['href']; else - $url = z_root() . '/magic?f=&owa=1&hash=' . $item['author_xchan'] . '&bdest=' . bin2hex($r['href'] . '/' . $revision); + $url = z_root() . '/magic?owa=1&bdest=' . bin2hex($r['href']); } - if (isset($label) && isset($url) && isset($icon) && isset($title)) { + if (isset($label, $url, $icon, $title)) { array_unshift($attaches, ['label' => $label, 'url' => $url, 'icon' => $icon, 'title' => $title]); } } @@ -1652,7 +1672,7 @@ function format_hashtags(&$item) { if($s) $s .= ' '; - $s .= '<span class="badge rounded-pill bg-info"><i class="fa fa-hashtag"></i> <a class="text-white" href="' . zid($t['url']) . '" >' . $term . '</a></span>'; + $s .= '<span class="badge rounded-pill bg-info"><i class="bi bi-hash"></i> <a class="text-white" href="' . zid($t['url']) . '" >' . $term . '</a></span>'; } } @@ -1675,7 +1695,7 @@ function format_mentions(&$item) { continue; if($s) $s .= ' '; - $s .= '<span class="badge rounded-pill bg-success"><i class="fa fa-at"></i> <a class="text-white" href="' . zid($t['url']) . '" >' . $term . '</a></span>'; + $s .= '<span class="badge rounded-pill bg-success"><i class="bi bi-at"></i> <a class="text-white" href="' . zid($t['url']) . '" >' . $term . '</a></span>'; } } @@ -1795,7 +1815,7 @@ function prepare_body(&$item,$attach = false,$opts = false) { $s .= prepare_text('[summary]' . $item['summary'] . '[/summary]' . $item['body'],$item['mimetype'],$opts); } else { - $s .= prepare_text($item['body'],$item['mimetype'], $opts); + $s .= prepare_text($item['body'],$item['mimetype'], $opts, $item['term'] ?? []); } } @@ -2004,7 +2024,7 @@ function format_poll($item,$s,$opts) { $message .= t('Poll has ended'); } else { - $message .= sprintf(t('Poll ends in %s'), '<span class="autotime" title="' . $t . '"></span>'); + $message .= sprintf(t('Poll ends %s'), '<span class="autotime" title="' . $t . '"></span>'); } } @@ -2041,8 +2061,7 @@ function format_poll($item,$s,$opts) { * @return string * The parsed $text as prepared HTML. */ -function prepare_text($text, $content_type = 'text/bbcode', $opts = false) { - +function prepare_text($text, $content_type = 'text/bbcode', $opts = false, $terms = []) { switch($content_type) { case 'text/plain': $s = escape_tags($text); @@ -2088,7 +2107,7 @@ function prepare_text($text, $content_type = 'text/bbcode', $opts = false) { $s = bbcode($text, ((is_array($opts)) ? $opts : [] )); } else { - $s = smilies(bbcode($text, ((is_array($opts)) ? $opts : [] ))); + $s = smilies(bbcode($text, ((is_array($opts)) ? $opts : [] )), terms: $terms); } } @@ -2168,7 +2187,7 @@ function get_plink($item,$conversation_mode = true) { if(array_key_exists('author',$item) && $item['author']['xchan_network'] !== 'zot6') $zidify = false; - if(x($item,$key)) { + if(!empty($item[$key])) { return array( 'href' => (($zidify) ? zid($item[$key]) : $item[$key]), 'title' => t('Link to Source'), @@ -2361,17 +2380,19 @@ function item_post_type($item) { $post_type = t('event'); break; default: - $post_type = t('post'); + $post_type = t('conversation'); if($item['mid'] != $item['parent_mid']) - $post_type = t('comment'); + $post_type = t('message'); break; } - if(strlen($item['verb']) && (! activity_match($item['verb'],ACTIVITY_POST))) + if(strlen($item['verb']) && (!activity_match($item['verb'], ['Create', ACTIVITY_POST]))) { $post_type = t('activity'); + } - if($item['obj_type'] === 'Question') + if($item['obj_type'] === 'Question') { $post_type = t('poll'); + } return $post_type; } @@ -2632,20 +2653,20 @@ function xchan_query(&$items, $abook = true, $effective_uid = 0) { $arr[] = "'" . dbesc($item['owner_xchan']) . "'"; if($item['author_xchan'] && (! in_array("'" . dbesc($item['author_xchan']) . "'",$arr))) $arr[] = "'" . dbesc($item['author_xchan']) . "'"; - if($item['source_xchan'] && (! in_array("'" . dbesc($item['source_xchan']) . "'",$arr))) + if(!empty($item['source_xchan']) && (! in_array("'" . dbesc($item['source_xchan']) . "'",$arr))) $arr[] = "'" . dbesc($item['source_xchan']) . "'"; } } if(count($arr)) { if($abook) { $chans = q("select * from xchan left join hubloc on hubloc_hash = xchan_hash left join abook on abook_xchan = xchan_hash and abook_channel = %d - where xchan_hash in (" . protect_sprintf(implode(',', $arr)) . ") and hubloc_deleted = 0 order by hubloc_primary desc", + where xchan_hash in (" . protect_sprintf(implode(',', $arr)) . ") order by hubloc_primary desc, hubloc_deleted ASC", intval($item['uid']) ); } else { $chans = q("select xchan.*,hubloc.* from xchan left join hubloc on hubloc_hash = xchan_hash - where xchan_hash in (" . protect_sprintf(implode(',', $arr)) . ") and hubloc_deleted = 0 order by hubloc_primary desc"); + where xchan_hash in (" . protect_sprintf(implode(',', $arr)) . ") order by hubloc_primary desc, hubloc_deleted ASC"); } $xchans = q("select * from xchan where xchan_hash in (" . protect_sprintf(implode(',',$arr)) . ") and xchan_network in ('rss','unknown', 'anon', 'token')"); if(! $chans) @@ -2657,7 +2678,9 @@ function xchan_query(&$items, $abook = true, $effective_uid = 0) { for($x = 0; $x < count($items); $x ++) { $items[$x]['owner'] = find_xchan_in_array($items[$x]['owner_xchan'],$chans); $items[$x]['author'] = find_xchan_in_array($items[$x]['author_xchan'],$chans); - $items[$x]['source'] = find_xchan_in_array($items[$x]['source_xchan'],$chans); + if (!empty($items[$x]['source_xchan'])) { + $items[$x]['source'] = find_xchan_in_array($items[$x]['source_xchan'],$chans); + } } } } @@ -3203,53 +3226,54 @@ function linkify_tags(&$body, $uid, $in_network = true) { function getIconFromType($type) { $iconMap = array( //Folder - 'Collection' => 'fa-folder-o', - 'multipart/mixed' => 'fa-folder-o', //dirs in attach use this mime type + 'Collection' => 'bi-folder', + 'multipart/mixed' => 'bi-folder', //dirs in attach use this mime type //Common file - 'application/octet-stream' => 'fa-file-o', + 'application/octet-stream' => 'bi-file-earmark', //Text - 'text/plain' => 'fa-file-text-o', - 'text/markdown' => 'fa-file-text-o', - 'text/bbcode' => 'fa-file-text-o', - 'text/html' => 'fa-file-text-o', - 'application/msword' => 'fa-file-word-o', - 'application/pdf' => 'fa-file-pdf-o', - 'application/vnd.oasis.opendocument.text' => 'fa-file-word-o', - 'application/epub+zip' => 'fa-book', + 'text/plain' => 'bi-file-earmark-text', + 'text/markdown' => 'bi-filetype-md', + 'text/bbcode' => 'bi-file-earmark-text', + 'text/html' => 'bi-filetype-html', + 'text/uri-list' => 'bi-box-arrow-up-right', + 'application/msword' => 'bi-file-earmark-word', + 'application/pdf' => 'bi-file-earmark-pdf', + 'application/vnd.oasis.opendocument.text' => 'bifile--earmark-text', + 'application/epub+zip' => 'bi-file-earmark-text', //Spreadsheet - 'application/vnd.oasis.opendocument.spreadsheet' => 'fa-file-excel-o', - 'application/vnd.ms-excel' => 'fa-file-excel-o', + 'application/vnd.oasis.opendocument.spreadsheet' => 'bi-file-earmark-spreadsheet', + 'application/vnd.ms-excel' => 'bi-file-earmark-spreadsheet', //Image - 'image/jpeg' => 'fa-picture-o', - 'image/png' => 'fa-picture-o', - 'image/gif' => 'fa-picture-o', - 'image/webp' => 'fa-picture-o', - 'image/svg+xml' => 'fa-picture-o', + 'image/jpeg' => 'bi-file-earmark-image', + 'image/png' => 'bi-file-earmark-image', + 'image/gif' => 'bi-file-earmark-image', + 'image/webp' => 'bi-file-earmark-image', + 'image/svg+xml' => 'bi-filetype-svg', //Archive - 'application/zip' => 'fa-file-archive-o', - 'application/x-rar-compressed' => 'fa-file-archive-o', + 'application/zip' => 'bi-file-earmark-zip', + 'application/x-rar-compressed' => 'bi-file-earmark-zip', //Audio - 'audio/mpeg' => 'fa-file-audio-o', - 'audio/wav' => 'fa-file-audio-o', - 'application/ogg' => 'fa-file-audio-o', - 'audio/ogg' => 'fa-file-audio-o', - 'audio/webm' => 'fa-file-audio-o', - 'audio/mp4' => 'fa-file-audio-o', + 'audio/mpeg' => 'bi-file-earmark-music', + 'audio/wav' => 'bi-file-earmark-music', + 'application/ogg' => 'bi-file-earmark-music', + 'audio/ogg' => 'bi-file-earmark-music', + 'audio/webm' => 'bi-file-earmark-music', + 'audio/mp4' => 'bi-file-earmark-music', //Video - 'video/quicktime' => 'fa-file-video-o', - 'video/webm' => 'fa-file-video-o', - 'video/mp4' => 'fa-file-video-o', - 'video/x-matroska' => 'fa-file-video-o' + 'video/quicktime' => 'bi-file-earmark-play', + 'video/webm' => 'bi-file-earmark-play', + 'video/mp4' => 'bi-file-earmark-play', + 'video/x-matroska' => 'bi-file-earmark-play' ); $catMap = [ - 'application' => 'fa-file-code-o', - 'multipart' => 'fa-folder', - 'audio' => 'fa-file-audio-o', - 'video' => 'fa-file-video-o', - 'text' => 'fa-file-text-o', - 'image' => 'fa=file-picture-o', - 'message' => 'fa-file-text-o' + 'application' => 'bi-file-earmark', + 'multipart' => 'bi-folder', + 'audio' => 'bi-file-earmark-music', + 'video' => 'bi-file-earmark-play', + 'text' => 'bi-file-earmark-text', + 'image' => 'bi-file-earmark-image', + 'message' => 'bi-file-earmark-text' ]; @@ -3266,7 +3290,7 @@ function getIconFromType($type) { } if(! $iconFromType) { - $iconFromType = 'fa-file-o'; + $iconFromType = 'bi-file-earmark'; } @@ -3755,12 +3779,9 @@ function cleanup_bbcode($body) { $body = preg_replace_callback('/\[img(.*?)\[\/(img)\]/ism','\red_escape_codeblock',$body); $body = preg_replace_callback('/\[zmg(.*?)\[\/(zmg)\]/ism','\red_escape_codeblock',$body); - $body = preg_replace_callback("/([^\]\='".'"'."\;\/\{]|^|\#\^)(https?\:\/\/[a-zA-Z0-9\pL\:\/\-\?\&\;\.\=\@\_\~\#\%\$\!\\ -+\,\(\)]+)/ismu", '\nakedoembed', $body); - - $body = preg_replace_callback("/([^\]\='".'"'."\;\/\{]|^|\#\^)(https?\:\/\/[a-zA-Z0-9\pL\:\/\-\?\&\;\.\=\@\_\~\#\%\$\!\\ -+\,\(\)]+)/ismu", '\red_zrl_callback', $body); + $body = preg_replace_callback("/([^\]\='".'"'."\;\/\{]|^|\#\^)(https?\:\/\/[a-zA-Z0-9\pL\:\/\-\?\&\;\.\=\@\_\~\#\%\$\!\\+\,\(\)]+)/ismu", '\nakedoembed', $body); + $body = preg_replace_callback("/([^\]\='".'"'."\;\/\{]|^|\#\^)(https?\:\/\/[a-zA-Z0-9\pL\:\/\-\?\&\;\.\=\@\_\~\#\%\$\!\\+\,\(\)]+)/ismu", '\red_zrl_callback', $body); $body = preg_replace_callback('/\[\$b64code(.*?)\[\/(code)\]/ism','\red_unescape_codeblock',$body); $body = preg_replace_callback('/\[\$b64summary(.*?)\[\/(summary)\]/ism','\red_unescape_codeblock',$body); @@ -3846,7 +3867,7 @@ function featured_sort($a,$b) { function unpunify($s) { - if (function_exists('idn_to_utf8') && isset($s)) { + if (function_exists('idn_to_utf8') && !empty($s)) { return idn_to_utf8($s); } return $s; @@ -3854,7 +3875,7 @@ function unpunify($s) { function punify($s) { - if (function_exists('idn_to_ascii') && isset($s)) { + if (function_exists('idn_to_ascii') && !empty($s)) { return idn_to_ascii($s); } return $s; diff --git a/include/xchan.php b/include/xchan.php index b8677c8c4..c492a77dc 100644 --- a/include/xchan.php +++ b/include/xchan.php @@ -217,6 +217,10 @@ function xchan_keychange_acl($table,$column,$oldxchan,$newxchan) { function xchan_change_key($oldx,$newx,$data) { + // TODO: this will need a refactor to eliminate duplicate keys + // E.G. item => [author_xchan, owner_xchan, source_kchan] + // Also: add a test! + $tables = [ 'abook' => 'abook_xchan', 'abconfig' => 'xchan', @@ -227,8 +231,6 @@ function xchan_change_key($oldx,$newx,$data) { 'item' => 'owner_xchan', 'item' => 'author_xchan', 'item' => 'source_xchan', - 'mail' => 'from_xchan', - 'mail' => 'to_xchan', 'shares' => 'share_xchan', 'source' => 'src_channel_xchan', 'source' => 'src_xchan', diff --git a/include/zid.php b/include/zid.php index 159a3b834..b74e82930 100644 --- a/include/zid.php +++ b/include/zid.php @@ -150,6 +150,9 @@ function clean_query_string($s = '') { */ function drop_query_params($s, $p) { + + $s = unescape_tags($s); + $parsed = parse_url($s); $query = ''; $query_args = null; @@ -172,7 +175,7 @@ function drop_query_params($s, $p) { $parsed['query'] = $query; } - return unparse_url($parsed); + return escape_tags(unparse_url($parsed)); } @@ -261,25 +264,25 @@ function zidify_text($s) { */ function red_zrl_callback($matches) { - // Catch and exclude trailing punctuation - preg_match("/[.,;:!?)]*$/i", $matches[2], $pts); - $matches[2] = substr($matches[2], 0, strlen($matches[2])-strlen($pts[0])); + // Catch and exclude trailing punctuation + preg_match("/[.,;:!?)]*$/i", $matches[2], $pts); + $matches[2] = substr($matches[2], 0, strlen($matches[2])-strlen($pts[0])); - $zrl = is_matrix_url($matches[2]); + $zrl = is_matrix_url($matches[2]); - $t = strip_zids($matches[2]); - if($t !== $matches[2]) { - $zrl = true; - $matches[2] = $t; - } + $t = strip_zids($matches[2]); + if($t !== $matches[2]) { + $zrl = true; + $matches[2] = $t; + } - if($matches[1] === '#^') - $matches[1] = ''; + if($matches[1] === '#^') + $matches[1] = ''; - if($zrl) - return $matches[1] . '#^[zrl=' . $matches[2] . ']' . $matches[2] . '[/zrl]' . $pts[0]; + if($zrl) + return $matches[1] . '#^[zrl=' . $matches[2] . ']' . $matches[2] . '[/zrl]' . $pts[0]; - return $matches[1] . '#^[url=' . $matches[2] . ']' . $matches[2] . '[/url]' . $pts[0]; + return $matches[1] . '#^[url=' . $matches[2] . ']' . $matches[2] . '[/url]' . $pts[0]; } /** |