Merge pull request #8146 from MrPetovan/task/7817-custom-fields-part-1

New custom profile fields feature part 1: Everything but custom profile fields
This commit is contained in:
Philipp 2020-01-20 21:26:23 +01:00 committed by GitHub
commit 0f0c58ddb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1016 additions and 719 deletions

View file

@ -494,7 +494,7 @@ function conversation(App $a, array $items, Pager $pager, $mode, $update, $previ
}
} elseif ($mode === 'profile') {
$items = conversation_add_children($items, false, $order, $uid);
$profile_owner = $a->profile['profile_uid'];
$profile_owner = $a->profile['uid'];
if (!$update) {
$tab = 'posts';
@ -508,7 +508,7 @@ function conversation(App $a, array $items, Pager $pager, $mode, $update, $previ
*/
$live_update_div = '<div id="live-profile"></div>' . "\r\n"
. "<script> var profile_uid = " . $a->profile['profile_uid']
. "<script> var profile_uid = " . $a->profile['uid']
. "; var netargs = '?f='; var profile_page = " . $pager->getPage() . "; </script>\r\n";
}
}

View file

@ -101,27 +101,14 @@ function cal_content(App $a)
}
// Setup permissions structures
$remote_contact = false;
$contact_id = 0;
$owner_uid = intval($a->data['user']['uid']);
$nick = $a->data['user']['nickname'];
if (!empty(Session::getRemoteContactID($a->profile['profile_uid']))) {
$contact_id = Session::getRemoteContactID($a->profile['profile_uid']);
}
$contact_id = Session::getRemoteContactID($a->profile['uid']);
if ($contact_id) {
$r = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d LIMIT 1",
intval($contact_id),
intval($a->profile['profile_uid'])
);
if (DBA::isResult($r)) {
$remote_contact = true;
}
}
$remote_contact = $contact_id && DBA::exists('contact', ['id' => $contact_id, 'uid' => $a->profile['uid']]);
$is_owner = local_user() == $a->profile['profile_uid'];
$is_owner = local_user() == $a->profile['uid'];
if ($a->profile['hidewall'] && !$is_owner && !$remote_contact) {
notice(DI::l10n()->t('Access to this profile has been restricted.') . EOL);

View file

@ -100,7 +100,7 @@ function display_init(App $a)
$nickname = str_replace(Strings::normaliseLink(DI::baseUrl())."/profile/", "", Strings::normaliseLink($profiledata["url"]));
if ($nickname != $a->user["nickname"]) {
$profile = DBA::fetchFirst("SELECT `profile`.`uid` AS `profile_uid`, `profile`.* , `contact`.`avatar-date` AS picdate, `user`.* FROM `profile`
$profile = DBA::fetchFirst("SELECT `profile`.* , `contact`.`avatar-date` AS picdate, `user`.* FROM `profile`
INNER JOIN `contact` on `contact`.`uid` = `profile`.`uid` INNER JOIN `user` ON `profile`.`uid` = `user`.`uid`
WHERE `user`.`nickname` = ? AND `profile`.`is-default` AND `contact`.`self` LIMIT 1",
$nickname
@ -175,9 +175,9 @@ function display_content(App $a, $update = false, $update_uid = 0)
$item_id = $_REQUEST['item_id'];
$item = Item::selectFirst(['uid', 'parent', 'parent-uri'], ['id' => $item_id]);
if ($item['uid'] != 0) {
$a->profile = ['uid' => intval($item['uid']), 'profile_uid' => intval($item['uid'])];
$a->profile = ['uid' => intval($item['uid'])];
} else {
$a->profile = ['uid' => intval($update_uid), 'profile_uid' => intval($update_uid)];
$a->profile = ['uid' => intval($update_uid)];
}
$item_parent = $item['parent'];
$item_parent_uri = $item['parent-uri'];
@ -249,13 +249,12 @@ function display_content(App $a, $update = false, $update_uid = 0)
if (DBA::isResult($parent)) {
$a->profile['uid'] = ($a->profile['uid'] ?? 0) ?: $parent['uid'];
$a->profile['profile_uid'] = ($a->profile['profile_uid'] ?? 0) ?: $parent['uid'];
$is_remote_contact = Session::getRemoteContactID($a->profile['profile_uid']);
$is_remote_contact = Session::getRemoteContactID($a->profile['uid']);
if ($is_remote_contact) {
$item_uid = $parent['uid'];
}
} else {
$a->profile = ['uid' => intval($item['uid']), 'profile_uid' => intval($item['uid'])];
$a->profile = ['uid' => intval($item['uid'])];
}
$page_contact = DBA::selectFirst('contact', [], ['self' => true, 'uid' => $a->profile['uid']]);
@ -263,7 +262,7 @@ function display_content(App $a, $update = false, $update_uid = 0)
$a->page_contact = $page_contact;
}
$is_owner = (local_user() && (in_array($a->profile['profile_uid'], [local_user(), 0])) ? true : false);
$is_owner = (local_user() && (in_array($a->profile['uid'], [local_user(), 0])) ? true : false);
if (!empty($a->profile['hidewall']) && !$is_owner && !$is_remote_contact) {
throw new HTTPException\ForbiddenException(DI::l10n()->t('Access to this profile has been restricted.'));
@ -284,9 +283,9 @@ function display_content(App $a, $update = false, $update_uid = 0)
];
$o .= status_editor($a, $x, 0, true);
}
$sql_extra = Item::getPermissionsSQLByUserId($a->profile['profile_uid']);
$sql_extra = Item::getPermissionsSQLByUserId($a->profile['uid']);
if (local_user() && (local_user() == $a->profile['profile_uid'])) {
if (local_user() && (local_user() == $a->profile['uid'])) {
$condition = ['parent-uri' => $item_parent_uri, 'uid' => local_user(), 'unseen' => true];
$unseen = Item::exists($condition);
} else {
@ -299,7 +298,7 @@ function display_content(App $a, $update = false, $update_uid = 0)
$condition = ["`id` = ? AND `item`.`uid` IN (0, ?) " . $sql_extra, $item_id, $item_uid];
$fields = ['parent-uri', 'body', 'title', 'author-name', 'author-avatar', 'plink', 'author-id', 'owner-id', 'contact-id'];
$item = Item::selectFirstForUser($a->profile['profile_uid'], $fields, $condition);
$item = Item::selectFirstForUser($a->profile['uid'], $fields, $condition);
if (!DBA::isResult($item)) {
throw new HTTPException\NotFoundException(DI::l10n()->t('The requested item doesn\'t exist or has been deleted.'));

View file

@ -1236,7 +1236,7 @@ function photos_content(App $a)
} else {
$tools['edit'] = ['photos/' . $a->data['user']['nickname'] . '/image/' . $datum . '/edit', DI::l10n()->t('Edit photo')];
$tools['delete'] = ['photos/' . $a->data['user']['nickname'] . '/image/' . $datum . '/drop', DI::l10n()->t('Delete photo')];
$tools['profile'] = ['profile_photo/use/'.$ph[0]['resource-id'], DI::l10n()->t('Use as profile photo')];
$tools['profile'] = ['settings/profile/photo/crop/' . $ph[0]['resource-id'], DI::l10n()->t('Use as profile photo')];
}
if (

View file

@ -1,324 +0,0 @@
<?php
/**
* @file mod/profile_photo.php
*/
use Friendica\App;
use Friendica\BaseModule;
use Friendica\Core\Renderer;
use Friendica\Core\Worker;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Photo;
use Friendica\Model\Profile;
use Friendica\Object\Image;
use Friendica\Util\Strings;
function profile_photo_init(App $a)
{
if (!local_user()) {
return;
}
Profile::load($a, $a->user['nickname']);
}
function profile_photo_post(App $a)
{
if (!local_user()) {
notice(DI::l10n()->t('Permission denied.') . EOL);
return;
}
BaseModule::checkFormSecurityTokenRedirectOnError('/profile_photo', 'profile_photo');
if (!empty($_POST['cropfinal']) && $_POST['cropfinal'] == 1) {
// unless proven otherwise
$is_default_profile = 1;
if ($_REQUEST['profile']) {
$r = q("select id, `is-default` from profile where id = %d and uid = %d limit 1", intval($_REQUEST['profile']),
intval(local_user())
);
if (DBA::isResult($r) && (!intval($r[0]['is-default']))) {
$is_default_profile = 0;
}
}
// phase 2 - we have finished cropping
if ($a->argc != 2) {
notice(DI::l10n()->t('Image uploaded but image cropping failed.') . EOL);
return;
}
$image_id = $a->argv[1];
if (substr($image_id, -2, 1) == '-') {
$scale = substr($image_id, -1, 1);
$image_id = substr($image_id, 0, -2);
}
$srcX = $_POST['xstart'];
$srcY = $_POST['ystart'];
$srcW = $_POST['xfinal'] - $srcX;
$srcH = $_POST['yfinal'] - $srcY;
$base_image = Photo::selectFirst([], ['resource-id' => $image_id, 'uid' => local_user(), 'scale' => $scale]);
$path = 'profile/' . $a->user['nickname'];
if (DBA::isResult($base_image)) {
$Image = Photo::getImageForPhoto($base_image);
if ($Image->isValid()) {
$Image->crop(300, $srcX, $srcY, $srcW, $srcH);
$r = Photo::store($Image, local_user(), 0, $base_image['resource-id'], $base_image['filename'],
DI::l10n()->t('Profile Photos'), 4, $is_default_profile);
if ($r === false) {
notice(DI::l10n()->t('Image size reduction [%s] failed.', "300") . EOL);
}
$Image->scaleDown(80);
$r = Photo::store($Image, local_user(), 0, $base_image['resource-id'], $base_image['filename'],
DI::l10n()->t('Profile Photos'), 5, $is_default_profile);
if ($r === false) {
notice(DI::l10n()->t('Image size reduction [%s] failed.', "80") . EOL);
}
$Image->scaleDown(48);
$r = Photo::store($Image, local_user(), 0, $base_image['resource-id'], $base_image['filename'],
DI::l10n()->t('Profile Photos'), 6, $is_default_profile);
if ($r === false) {
notice(DI::l10n()->t('Image size reduction [%s] failed.', "48") . EOL);
}
// If setting for the default profile, unset the profile photo flag from any other photos I own
if ($is_default_profile) {
q("UPDATE `photo` SET `profile` = 0 WHERE `profile` = 1 AND `resource-id` != '%s' AND `uid` = %d",
DBA::escape($base_image['resource-id']), intval(local_user())
);
} else {
q("update profile set photo = '%s', thumb = '%s' where id = %d and uid = %d",
DBA::escape(DI::baseUrl() . '/photo/' . $base_image['resource-id'] . '-4.' . $Image->getExt()),
DBA::escape(DI::baseUrl() . '/photo/' . $base_image['resource-id'] . '-5.' . $Image->getExt()),
intval($_REQUEST['profile']), intval(local_user())
);
}
Contact::updateSelfFromUserID(local_user(), true);
info(DI::l10n()->t('Shift-reload the page or clear browser cache if the new photo does not display immediately.') . EOL);
// Update global directory in background
if ($path && strlen(DI::config()->get('system', 'directory'))) {
Worker::add(PRIORITY_LOW, "Directory", DI::baseUrl()->get() . '/' . $path);
}
Worker::add(PRIORITY_LOW, 'ProfileUpdate', local_user());
} else {
notice(DI::l10n()->t('Unable to process image') . EOL);
}
}
DI::baseUrl()->redirect($path);
return; // NOTREACHED
}
$src = $_FILES['userfile']['tmp_name'];
$filename = basename($_FILES['userfile']['name']);
$filesize = intval($_FILES['userfile']['size']);
$filetype = $_FILES['userfile']['type'];
if ($filetype == "") {
$filetype = Image::guessType($filename);
}
$maximagesize = DI::config()->get('system', 'maximagesize');
if (($maximagesize) && ($filesize > $maximagesize)) {
notice(DI::l10n()->t('Image exceeds size limit of %s', Strings::formatBytes($maximagesize)) . EOL);
@unlink($src);
return;
}
$imagedata = @file_get_contents($src);
$ph = new Image($imagedata, $filetype);
if (!$ph->isValid()) {
notice(DI::l10n()->t('Unable to process image.') . EOL);
@unlink($src);
return;
}
$ph->orient($src);
@unlink($src);
$imagecrop = profile_photo_crop_ui_head($ph);
DI::baseUrl()->redirect('profile_photo/use/' . $imagecrop['hash']);
}
function profile_photo_content(App $a)
{
if (!local_user()) {
notice(DI::l10n()->t('Permission denied.') . EOL);
return;
}
$newuser = false;
if ($a->argc == 2 && $a->argv[1] === 'new') {
$newuser = true;
}
$imagecrop = [];
if (isset($a->argv[1]) && $a->argv[1] == 'use' && $a->argc >= 3) {
// BaseModule::checkFormSecurityTokenRedirectOnError('/profile_photo', 'profile_photo');
$resource_id = $a->argv[2];
//die(":".local_user());
$r = Photo::selectToArray([], ["resource-id" => $resource_id, "uid" => local_user()], ["order" => ["scale" => false]]);
if (!DBA::isResult($r)) {
notice(DI::l10n()->t('Permission denied.') . EOL);
return;
}
$havescale = false;
foreach ($r as $rr) {
if ($rr['scale'] == 5) {
$havescale = true;
}
}
// set an already uloaded photo as profile photo
// if photo is in 'Profile Photos', change it in db
if (($r[0]['album'] == DI::l10n()->t('Profile Photos')) && ($havescale)) {
q("UPDATE `photo` SET `profile`=0 WHERE `profile`=1 AND `uid`=%d", intval(local_user()));
q("UPDATE `photo` SET `profile`=1 WHERE `uid` = %d AND `resource-id` = '%s'", intval(local_user()),
DBA::escape($resource_id)
);
Contact::updateSelfFromUserID(local_user(), true);
// Update global directory in background
$url = $_SESSION['my_url'];
if ($url && strlen(DI::config()->get('system', 'directory'))) {
Worker::add(PRIORITY_LOW, "Directory", $url);
}
DI::baseUrl()->redirect('profile/' . $a->user['nickname']);
return; // NOTREACHED
}
$ph = Photo::getImageForPhoto($r[0]);
$imagecrop = profile_photo_crop_ui_head($ph);
// go ahead as we have jus uploaded a new photo to crop
}
$profiles = q("select `id`,`profile-name` as `name`,`is-default` as `default` from profile where uid = %d",
intval(local_user())
);
if (empty($imagecrop)) {
$tpl = Renderer::getMarkupTemplate('profile_photo.tpl');
$o = Renderer::replaceMacros($tpl,
[
'$user' => $a->user['nickname'],
'$lbl_upfile' => DI::l10n()->t('Upload File:'),
'$lbl_profiles' => DI::l10n()->t('Select a profile:'),
'$title' => DI::l10n()->t('Upload Profile Photo'),
'$submit' => DI::l10n()->t('Upload'),
'$profiles' => $profiles,
'$form_security_token' => BaseModule::getFormSecurityToken("profile_photo"),
'$select' => sprintf('%s %s', DI::l10n()->t('or'),
($newuser) ? '<a href="' . DI::baseUrl() . '">' . DI::l10n()->t('skip this step') . '</a>' : '<a href="' . DI::baseUrl() . '/photos/' . $a->user['nickname'] . '">' . DI::l10n()->t('select a photo from your photo albums') . '</a>')
]);
return $o;
} else {
$filename = $imagecrop['hash'] . '-' . $imagecrop['resolution'] . '.' . $imagecrop['ext'];
$tpl = Renderer::getMarkupTemplate("cropbody.tpl");
$o = Renderer::replaceMacros($tpl,
[
'$filename' => $filename,
'$profile' => (isset($_REQUEST['profile']) ? intval($_REQUEST['profile']) : 0),
'$resource' => $imagecrop['hash'] . '-' . $imagecrop['resolution'],
'$image_url' => DI::baseUrl() . '/photo/' . $filename,
'$title' => DI::l10n()->t('Crop Image'),
'$desc' => DI::l10n()->t('Please adjust the image cropping for optimum viewing.'),
'$form_security_token' => BaseModule::getFormSecurityToken("profile_photo"),
'$done' => DI::l10n()->t('Done Editing')
]);
return $o;
}
}
function profile_photo_crop_ui_head(Image $image)
{
$max_length = DI::config()->get('system', 'max_image_length');
if (!$max_length) {
$max_length = MAX_IMAGE_LENGTH;
}
if ($max_length > 0) {
$image->scaleDown($max_length);
}
$width = $image->getWidth();
$height = $image->getHeight();
if ($width < 175 || $height < 175) {
$image->scaleUp(300);
$width = $image->getWidth();
$height = $image->getHeight();
}
$hash = Photo::newResource();
$smallest = 0;
$filename = '';
$r = Photo::store($image, local_user(), 0, $hash, $filename, DI::l10n()->t('Profile Photos'), 0);
if ($r) {
info(DI::l10n()->t('Image uploaded successfully.') . EOL);
} else {
notice(DI::l10n()->t('Image upload failed.') . EOL);
}
if ($width > 640 || $height > 640) {
$image->scaleDown(640);
$r = Photo::store($image, local_user(), 0, $hash, $filename, DI::l10n()->t('Profile Photos'), 1);
if ($r === false) {
notice(DI::l10n()->t('Image size reduction [%s] failed.', "640") . EOL);
} else {
$smallest = 1;
}
}
DI::page()['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate("crophead.tpl"), []);
$imagecrop = [
'hash' => $hash,
'resolution' => $smallest,
'ext' => $image->getExt(),
];
return $imagecrop;
}

View file

@ -598,7 +598,7 @@ function profiles_content(App $a) {
'$region' => ['region', DI::l10n()->t('Region/State:'), $r[0]['region']],
'$postal_code' => ['postal_code', DI::l10n()->t('Postal/Zip Code:'), $r[0]['postal-code']],
'$country_name' => ['country_name', DI::l10n()->t('Country:'), $r[0]['country-name']],
'$age' => ((intval($r[0]['dob'])) ? '(' . DI::l10n()->t('Age: ') . Temporal::getAgeByTimezone($r[0]['dob'],$a->user['timezone'],$a->user['timezone']) . ')' : ''),
'$age' => ((intval($r[0]['dob'])) ? '(' . DI::l10n()->t('Age: ') . DI::l10n()->tt('%d year old', '%d years old', Temporal::getAgeByTimezone($r[0]['dob'], $a->user['timezone'])) . ')' : ''),
'$gender' => DI::l10n()->t(ContactSelector::gender($r[0]['gender'])),
'$marital' => ['selector' => ContactSelector::maritalStatus($r[0]['marital']), 'value' => DI::l10n()->t($r[0]['marital'])],
'$with' => ['with', DI::l10n()->t("Who: \x28if applicable\x29"), strip_tags($r[0]['with']), DI::l10n()->t('Examples: cathy123, Cathy Williams, cathy@example.com')],

View file

@ -373,7 +373,7 @@ class Authentication
if ($user_record['login_date'] <= DBA::NULL_DATETIME) {
info($this->l10n->t('Welcome %s', $user_record['username']));
info($this->l10n->t('Please upload a profile photo.'));
$this->baseUrl->redirect('profile_photo/new');
$this->baseUrl->redirect('settings/profile/photo/new');
} else {
info($this->l10n->t("Welcome back %s", $user_record['username']));
}

View file

@ -28,6 +28,30 @@ abstract class BaseCollection extends \ArrayIterator
$this->totalCount = $totalCount ?? count($models);
}
/**
* @inheritDoc
*/
public function offsetSet($offset, $value)
{
if (is_null($offset)) {
$this->totalCount++;
}
parent::offsetSet($offset, $value);
}
/**
* @inheritDoc
*/
public function offsetUnset($offset)
{
if ($this->offsetExists($offset)) {
$this->totalCount--;
}
parent::offsetUnset($offset);
}
/**
* @return int
*/
@ -35,4 +59,42 @@ abstract class BaseCollection extends \ArrayIterator
{
return $this->totalCount;
}
/**
* Return the values from a single field in the collection
*
* @param string $column
* @param int|null $index_key
* @return array
* @see array_column()
*/
public function column($column, $index_key = null)
{
return array_column($this->getArrayCopy(), $column, $index_key);
}
/**
* Apply a callback function on all elements in the collection and returns a new collection with the updated elements
*
* @param callable $callback
* @return BaseCollection
* @see array_map()
*/
public function map(callable $callback)
{
return new static(array_map($callback, $this->getArrayCopy()), $this->getTotalCount());
}
/**
* Filters the collection based on a callback that returns a boolean whether the current item should be kept.
*
* @param callable|null $callback
* @param int $flag
* @return BaseCollection
* @see array_filter()
*/
public function filter(callable $callback = null, int $flag = 0)
{
return new static(array_filter($this->getArrayCopy(), $callback, $flag));
}
}

View file

@ -28,6 +28,13 @@ abstract class BaseModel
*/
private $data = [];
/**
* Used to limit/avoid updates if no data was changed.
*
* @var array
*/
private $originalData = [];
/**
* @param Database $dba
* @param LoggerInterface $logger
@ -38,6 +45,12 @@ abstract class BaseModel
$this->dba = $dba;
$this->logger = $logger;
$this->data = $data;
$this->originalData = $data;
}
public function getOriginalData()
{
return $this->originalData;
}
/**
@ -51,10 +64,23 @@ abstract class BaseModel
{
$model = clone $prototype;
$model->data = $data;
$model->originalData = $data;
return $model;
}
/**
* Magic isset method. Returns true if the field exists, either in the data prperty array or in any of the local properties.
* Used by array_column() on an array of objects.
*
* @param $name
* @return bool
*/
public function __isset($name)
{
return in_array($name, array_merge(array_keys($this->data), array_keys(get_object_vars($this))));
}
/**
* Magic getter. This allows to retrieve model fields with the following syntax:
* - $model->field (outside of class)
@ -66,9 +92,7 @@ abstract class BaseModel
*/
public function __get($name)
{
if (empty($this->data['id'])) {
throw new HTTPException\InternalServerErrorException(static::class . ' record uninitialized');
}
$this->checkValid();
if (!array_key_exists($name, $this->data)) {
throw new HTTPException\InternalServerErrorException('Field ' . $name . ' not found in ' . static::class);
@ -90,4 +114,11 @@ abstract class BaseModel
{
return $this->data;
}
protected function checkValid()
{
if (empty($this->data['id'])) {
throw new HTTPException\InternalServerErrorException(static::class . ' record uninitialized');
}
}
}

View file

@ -61,8 +61,7 @@ abstract class BaseModule
*/
public static function post(array $parameters = [])
{
// $a = self::getApp();
// $a->internalRedirect('module');
// DI::baseurl()->redirect('module');
}
/**

View file

@ -122,7 +122,7 @@ abstract class BaseRepository extends BaseFactory
*/
public function update(BaseModel $model)
{
return $this->dba->update(static::$table_name, $model->toArray(), ['id' => $model->id], true);
return $this->dba->update(static::$table_name, $model->toArray(), ['id' => $model->id], $model->getOriginalData());
}
/**
@ -136,10 +136,12 @@ abstract class BaseRepository extends BaseFactory
{
$return = $this->dba->insert(static::$table_name, $fields);
if ($return) {
if (!$return) {
throw new HTTPException\InternalServerErrorException('Unable to insert new row in table "' . static::$table_name . '"');
}
$fields['id'] = $this->dba->lastInsertId();
$return = $this->create($fields);
}
return $return;
}
@ -197,4 +199,12 @@ abstract class BaseRepository extends BaseFactory
return $models;
}
/**
* @param BaseCollection $collection
*/
public function saveCollection(BaseCollection $collection)
{
$collection->map([$this, 'update']);
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Friendica\Collection;
use Friendica\BaseCollection;
class PermissionSets extends BaseCollection
{
}

View file

@ -5,8 +5,8 @@
*/
namespace Friendica\Content;
use Friendica\Core\Protocol;
use Friendica\Content\Text\HTML;
use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Database\DBA;
use Friendica\DI;
@ -153,7 +153,7 @@ class ForumManager
{
$profile = intval(Feature::isEnabled($uid, 'forumlist_profile'));
if (!$profile) {
return;
return '';
}
$o = '';
@ -167,20 +167,16 @@ class ForumManager
$contacts = self::getList($uid, $lastitem, false, false);
$total_shown = 0;
$forumlist = '';
foreach ($contacts as $contact) {
$forumlist .= HTML::micropro($contact, true, 'forumlist-profile-advanced');
$o .= HTML::micropro($contact, true, 'forumlist-profile-advanced');
$total_shown++;
if ($total_shown == $show_total) {
break;
}
}
if (count($contacts) > 0) {
$o .= $forumlist;
return $o;
}
}
/**
* count unread forum items

View file

@ -301,7 +301,7 @@ class Widget
{
$a = DI::app();
$uid = intval($a->profile['profile_uid']);
$uid = intval($a->profile['uid']);
if (!Feature::isEnabled($uid, 'categories')) {
return '';
@ -418,7 +418,7 @@ class Widget
{
$a = DI::app();
$uid = intval($a->profile['profile_uid']);
$uid = intval($a->profile['uid']);
if (!$uid || !$a->profile['url']) {
return '';

View file

@ -229,18 +229,33 @@ class ACL
* Returns the ACL list of contacts for a given user id
*
* @param int $user_id
* @param array $condition Additional contact lookup table conditions
* @return array
* @throws \Exception
*/
public static function getContactListByUserId(int $user_id)
public static function getContactListByUserId(int $user_id, array $condition = [])
{
$fields = ['id', 'name', 'addr', 'micro'];
$params = ['order' => ['name']];
$acl_contacts = Contact::selectToArray($fields,
['uid' => $user_id, 'self' => false, 'blocked' => false, 'archive' => false, 'deleted' => false,
'pending' => false, 'rel' => [Contact::FOLLOWER, Contact::FRIEND]], $params
$acl_contacts = Contact::selectToArray(
$fields,
array_merge([
'uid' => $user_id,
'self' => false,
'blocked' => false,
'archive' => false,
'deleted' => false,
'pending' => false,
'rel' => [Contact::FOLLOWER, Contact::FRIEND]
], $condition),
$params
);
$acl_yourself = Contact::selectFirst($fields, ['uid' => $user_id, 'self' => true]);
$acl_yourself['name'] = DI::l10n()->t('Yourself');
$acl_contacts[] = $acl_yourself;
$acl_forums = Contact::selectToArray($fields,
['uid' => $user_id, 'self' => false, 'blocked' => false, 'archive' => false, 'deleted' => false,
'pending' => false, 'contact-type' => Contact::TYPE_COMMUNITY], $params
@ -306,15 +321,27 @@ class ACL
* 'deny_gid' => [],
* 'hidewall' => true/false
* ]
* @param array $condition
* @param string $form_prefix
* @return string
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function getFullSelectorHTML(Page $page, array $user = null, bool $for_federation = false, array $default_permissions = [])
{
public static function getFullSelectorHTML(
Page $page,
array $user = null,
bool $for_federation = false,
array $default_permissions = [],
array $condition = [],
$form_prefix = ''
) {
if (empty($user['uid'])) {
return '';
}
static $input_group_id = 0;
$input_group_id++;
$page->registerFooterScript(Theme::getPathForFile('asset/typeahead.js/dist/typeahead.bundle.js'));
$page->registerFooterScript(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.js'));
$page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.css'));
@ -373,12 +400,21 @@ class ACL
}
}
$acl_contacts = self::getContactListByUserId($user['uid']);
$acl_contacts = self::getContactListByUserId($user['uid'], $condition);
$acl_groups = self::getGroupListByUserId($user['uid']);
$acl_list = array_merge($acl_groups, $acl_contacts);
$input_names = [
'visibility' => $form_prefix ? $form_prefix . '[visibility]' : 'visibility',
'group_allow' => $form_prefix ? $form_prefix . '[group_allow]' : 'group_allow',
'contact_allow' => $form_prefix ? $form_prefix . '[contact_allow]' : 'contact_allow',
'group_deny' => $form_prefix ? $form_prefix . '[group_deny]' : 'group_deny',
'contact_deny' => $form_prefix ? $form_prefix . '[contact_deny]' : 'contact_deny',
'emailcc' => $form_prefix ? $form_prefix . '[emailcc]' : 'emailcc',
];
$tpl = Renderer::getMarkupTemplate('acl_selector.tpl');
$o = Renderer::replaceMacros($tpl, [
'$public_title' => DI::l10n()->t('Public'),
@ -402,6 +438,8 @@ class ACL
'$for_federation' => $for_federation,
'$jotnets_fields' => $jotnets_fields,
'$user_hidewall' => $default_permissions['hidewall'],
'$input_names' => $input_names,
'$input_group_id' => $input_group_id,
]);
return $o;

View file

@ -61,7 +61,7 @@ class Session
$session = DI::session();
if (empty($session->get('remote')[$uid])) {
return false;
return 0;
}
return $session->get('remote')[$uid];

View file

@ -284,6 +284,14 @@ abstract class DI
return self::$dice->create(Repository\Introduction::class);
}
/**
* @return Repository\PermissionSet
*/
public static function permissionSet()
{
return self::$dice->create(Repository\PermissionSet::class);
}
//
// "Protocol" namespace instances
//

View file

@ -1342,19 +1342,13 @@ class Database
}
}
$do_update = (count($old_fields) == 0);
foreach ($old_fields AS $fieldname => $content) {
if (isset($fields[$fieldname])) {
if (($fields[$fieldname] == $content) && !is_null($content)) {
if (isset($fields[$fieldname]) && !is_null($content) && ($fields[$fieldname] == $content)) {
unset($fields[$fieldname]);
} else {
$do_update = true;
}
}
}
if (!$do_update || (count($fields) == 0)) {
if (count($fields) == 0) {
return true;
}

View file

@ -10,11 +10,14 @@ use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Item;
use Friendica\Model\ItemURI;
use Friendica\Model\UserItem;
use Friendica\Model\PermissionSet;
use Friendica\Model\UserItem;
/**
* Post update functions
* These database-intensive post update routines are meant to be executed in the background by the cronjob.
*
* If there is a need for a intensive migration after a database structure change, update this file
* by adding a new method at the end with the number of the new DB_UPDATE_VERSION.
*/
class PostUpdate
{
@ -204,13 +207,19 @@ class PostUpdate
}
if (empty($item['psid'])) {
$item['psid'] = PermissionSet::fetchIDForPost($item);
} else {
$item['psid'] = PermissionSet::getIdFromACL(
$item['uid'],
$item['allow_cid'],
$item['allow_gid'],
$item['deny_cid'],
$item['deny_gid']
);
}
$item['allow_cid'] = null;
$item['allow_gid'] = null;
$item['deny_cid'] = null;
$item['deny_gid'] = null;
}
if ($item['post-type'] == 0) {
if (!empty($item['type']) && ($item['type'] == 'note')) {

View file

@ -117,6 +117,16 @@ class Group
}
DBA::close($stmt);
// Meta-groups
$contact = Contact::getById($cid, ['rel']);
if ($contact['rel'] == Contact::FOLLOWER || $contact['rel'] == Contact::FRIEND) {
$return[] = self::FOLLOWERS;
}
if ($contact['rel'] == Contact::FRIEND) {
$return[] = self::MUTUALS;
}
return $return;
}

View file

@ -1856,7 +1856,18 @@ class Item
}
// Creates or assigns the permission set
$item['psid'] = PermissionSet::fetchIDForPost($item);
$item['psid'] = PermissionSet::getIdFromACL(
$item['uid'],
$item['allow_cid'],
$item['allow_gid'],
$item['deny_cid'],
$item['deny_gid']
);
$item['allow_cid'] = null;
$item['allow_gid'] = null;
$item['deny_cid'] = null;
$item['deny_gid'] = null;
// We are doing this outside of the transaction to avoid timing problems
if (!self::insertActivity($item)) {
@ -2729,7 +2740,13 @@ class Item
$private = ($user['allow_cid'] || $user['allow_gid'] || $user['deny_cid'] || $user['deny_gid']) ? 1 : 0;
$psid = PermissionSet::fetchIDForPost($user);
$psid = PermissionSet::getIdFromACL(
$user['uid'],
$user['allow_cid'],
$user['allow_gid'],
$user['deny_cid'],
$user['deny_gid']
);
$forum_mode = ($prvgroup ? 2 : 1);

View file

@ -2,63 +2,38 @@
/**
* @file src/Model/PermissionSet.php
*/
namespace Friendica\Model;
use Friendica\Database\DBA;
use Friendica\BaseModel;
use Friendica\DI;
/**
* functions for interacting with the permission set of an object (item, photo, event, ...)
*/
class PermissionSet
class PermissionSet extends BaseModel
{
/**
* Fetch the id of a given permission set. Generate a new one when needed
*
* @param array $postarray The array from an item, picture or event post
* @param int $uid
* @param string|null $allow_cid Allowed contact IDs - empty = everyone
* @param string|null $allow_gid Allowed group IDs - empty = everyone
* @param string|null $deny_cid Disallowed contact IDs - empty = no one
* @param string|null $deny_gid Disallowed group IDs - empty = no one
* @return int id
* @throws \Exception
* @deprecated since 2020.03, use Repository\PermissionSet instead
* @see \Friendica\Repository\PermissionSet->getIdFromACL
*/
public static function fetchIDForPost(&$postarray)
{
$condition = ['uid' => $postarray['uid'],
'allow_cid' => self::sortPermissions($postarray['allow_cid'] ?? ''),
'allow_gid' => self::sortPermissions($postarray['allow_gid'] ?? ''),
'deny_cid' => self::sortPermissions($postarray['deny_cid'] ?? ''),
'deny_gid' => self::sortPermissions($postarray['deny_gid'] ?? '')];
$set = DBA::selectFirst('permissionset', ['id'], $condition);
if (!DBA::isResult($set)) {
DBA::insert('permissionset', $condition, true);
$set = DBA::selectFirst('permissionset', ['id'], $condition);
}
$postarray['allow_cid'] = null;
$postarray['allow_gid'] = null;
$postarray['deny_cid'] = null;
$postarray['deny_gid'] = null;
return $set['id'];
}
private static function sortPermissions($permissionlist)
{
$cleaned_list = trim($permissionlist, '<>');
if (empty($cleaned_list)) {
return $permissionlist;
}
$elements = explode('><', $cleaned_list);
if (count($elements) <= 1) {
return $permissionlist;
}
asort($elements);
return '<' . implode('><', $elements) . '>';
public static function getIdFromACL(
int $uid,
string $allow_cid = null,
string $allow_gid = null,
string $deny_cid = null,
string $deny_gid = null
) {
return DI::permissionSet()->getIdFromACL($uid, $allow_cid, $allow_gid, $deny_cid, $deny_gid);
}
/**
@ -69,36 +44,13 @@ class PermissionSet
*
* @return array of permission set ids.
* @throws \Exception
* @deprecated since 2020.03, use Repository\PermissionSet instead
* @see \Friendica\Repository\PermissionSet->selectByContactId
*/
static public function get($uid, $contact_id)
public static function get($uid, $contact_id)
{
if (DBA::exists('contact', ['id' => $contact_id, 'uid' => $uid, 'blocked' => false])) {
$groups = Group::getIdsByContactId($contact_id);
}
$permissionSets = DI::permissionSet()->selectByContactId($contact_id, $uid);
if (empty($groups) || !is_array($groups)) {
return [];
}
$group_str = '<<>>'; // should be impossible to match
foreach ($groups as $g) {
$group_str .= '|<' . intval($g) . '>';
}
$contact_str = '<' . $contact_id . '>';
$condition = ["`uid` = ? AND (NOT (`deny_cid` REGEXP ? OR deny_gid REGEXP ?)
AND (allow_cid REGEXP ? OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))",
$uid, $contact_str, $group_str, $contact_str, $group_str];
$ret = DBA::select('permissionset', ['id'], $condition);
$set = [];
while ($permission = DBA::fetch($ret)) {
$set[] = $permission['id'];
}
DBA::close($ret);
return $set;
return $permissionSets->column('id');
}
}

View file

@ -126,13 +126,13 @@ class Profile
*
* @param App $a
* @param string $nickname string
* @param int $profile int
* @param int $profile_id int
* @param array $profiledata array
* @param boolean $show_connect Show connect link
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function load(App $a, $nickname, $profile = 0, array $profiledata = [], $show_connect = true)
public static function load(App $a, $nickname, $profile_id = 0, array $profiledata = [], $show_connect = true)
{
$user = DBA::selectFirst('user', ['uid'], ['nickname' => $nickname, 'account_removed' => false]);
@ -155,31 +155,31 @@ class Profile
}
}
$pdata = self::getByNickname($nickname, $user['uid'], $profile);
$profile = self::getByNickname($nickname, $user['uid'], $profile_id);
if (empty($pdata) && empty($profiledata)) {
if (empty($profile) && empty($profiledata)) {
Logger::log('profile error: ' . DI::args()->getQueryString(), Logger::DEBUG);
return;
}
if (empty($pdata)) {
$pdata = ['uid' => 0, 'profile_uid' => 0, 'is-default' => false,'name' => $nickname];
if (empty($profile)) {
$profile = ['uid' => 0, 'is-default' => false,'name' => $nickname];
}
// fetch user tags if this isn't the default profile
if (!$pdata['is-default']) {
$condition = ['uid' => $pdata['profile_uid'], 'is-default' => true];
$profile = DBA::selectFirst('profile', ['pub_keywords'], $condition);
if (DBA::isResult($profile)) {
$pdata['pub_keywords'] = $profile['pub_keywords'];
if (!$profile['is-default']) {
$condition = ['uid' => $profile['uid'], 'is-default' => true];
$profile_id = DBA::selectFirst('profile', ['pub_keywords'], $condition);
if (DBA::isResult($profile_id)) {
$profile['pub_keywords'] = $profile_id['pub_keywords'];
}
}
$a->profile = $pdata;
$a->profile_uid = $pdata['profile_uid'];
$a->profile = $profile;
$a->profile_uid = $profile['uid'];
$a->profile['mobile-theme'] = DI::pConfig()->get($a->profile['profile_uid'], 'system', 'mobile_theme');
$a->profile['mobile-theme'] = DI::pConfig()->get($a->profile['uid'], 'system', 'mobile_theme');
$a->profile['network'] = Protocol::DFRN;
DI::page()['title'] = $a->profile['name'] . ' @ ' . DI::config()->get('config', 'sitename');
@ -255,7 +255,7 @@ class Profile
$profile = DBA::fetchFirst(
"SELECT `contact`.`id` AS `contact_id`, `contact`.`photo` AS `contact_photo`,
`contact`.`thumb` AS `contact_thumb`, `contact`.`micro` AS `contact_micro`,
`profile`.`uid` AS `profile_uid`, `profile`.*,
`profile`.*,
`contact`.`avatar-date` AS picdate, `contact`.`addr`, `contact`.`url`, `user`.*
FROM `profile`
INNER JOIN `contact` on `contact`.`uid` = `profile`.`uid` AND `contact`.`self`
@ -269,7 +269,7 @@ class Profile
$profile = DBA::fetchFirst(
"SELECT `contact`.`id` AS `contact_id`, `contact`.`photo` as `contact_photo`,
`contact`.`thumb` AS `contact_thumb`, `contact`.`micro` AS `contact_micro`,
`profile`.`uid` AS `profile_uid`, `profile`.*,
`profile`.*,
`contact`.`avatar-date` AS picdate, `contact`.`addr`, `contact`.`url`, `user`.*
FROM `profile`
INNER JOIN `contact` ON `contact`.`uid` = `profile`.`uid` AND `contact`.`self`
@ -350,7 +350,7 @@ class Profile
$profile_is_dfrn = $profile['network'] == Protocol::DFRN;
$profile_is_native = in_array($profile['network'], Protocol::NATIVE_SUPPORT);
$local_user_is_self = local_user() && local_user() == ($profile['profile_uid'] ?? 0);
$local_user_is_self = local_user() && local_user() == ($profile['uid'] ?? 0);
$visitor_is_authenticated = (bool)self::getMyURL();
$visitor_is_following =
in_array($visitor_contact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND])
@ -527,7 +527,7 @@ class Profile
$p['url'] = Contact::magicLink(($p['url'] ?? '') ?: $profile_url);
$tpl = Renderer::getMarkupTemplate('profile_vcard.tpl');
$tpl = Renderer::getMarkupTemplate('profile/vcard.tpl');
$o .= Renderer::replaceMacros($tpl, [
'$profile' => $p,
'$xmpp' => $xmpp,
@ -747,8 +747,6 @@ class Profile
$uid = intval($a->profile['uid']);
if ($a->profile['name']) {
$tpl = Renderer::getMarkupTemplate('profile_advanced.tpl');
$profile = [];
$profile['fullname'] = [DI::l10n()->t('Full Name:'), $a->profile['name']];
@ -776,9 +774,9 @@ class Profile
if (!empty($a->profile['dob'])
&& $a->profile['dob'] > DBA::NULL_DATE
&& $age = Temporal::getAgeByTimezone($a->profile['dob'], $a->profile['timezone'], '')
&& $age = Temporal::getAgeByTimezone($a->profile['dob'], $a->profile['timezone'])
) {
$profile['age'] = [DI::l10n()->t('Age:'), $age];
$profile['age'] = [DI::l10n()->t('Age: ') , DI::l10n()->tt('%d year old', '%d years old', $age)];
}
if ($a->profile['marital']) {
@ -875,6 +873,7 @@ class Profile
$profile['edit'] = [DI::baseUrl() . '/profiles/' . $a->profile['id'], DI::l10n()->t('Edit profile'), '', DI::l10n()->t('Edit profile')];
}
$tpl = Renderer::getMarkupTemplate('profile/advanced.tpl');
return Renderer::replaceMacros($tpl, [
'$title' => DI::l10n()->t('Profile'),
'$basic' => DI::l10n()->t('Basic'),

View file

@ -82,8 +82,8 @@ class Profile extends BaseModule
$page['htmlhead'] .= "\n";
$blocked = !local_user() && !Session::getRemoteContactID($a->profile['profile_uid']) && DI::config()->get('system', 'block_public');
$userblock = !local_user() && !Session::getRemoteContactID($a->profile['profile_uid']) && $a->profile['hidewall'];
$blocked = !local_user() && !Session::getRemoteContactID($a->profile['uid']) && DI::config()->get('system', 'block_public');
$userblock = !local_user() && !Session::getRemoteContactID($a->profile['uid']) && $a->profile['hidewall'];
if (!empty($a->profile['page-flags']) && $a->profile['page-flags'] == User::PAGE_FLAGS_COMMUNITY) {
$page['htmlhead'] .= '<meta name="friendica.community" content="true" />' . "\n";
@ -149,7 +149,7 @@ class Profile extends BaseModule
$hashtags = $_GET['tag'] ?? '';
if (DI::config()->get('system', 'block_public') && !local_user() && !Session::getRemoteContactID($a->profile['profile_uid'])) {
if (DI::config()->get('system', 'block_public') && !local_user() && !Session::getRemoteContactID($a->profile['uid'])) {
return Login::form();
}
@ -157,14 +157,14 @@ class Profile extends BaseModule
if ($update) {
// Ensure we've got a profile owner if updating.
$a->profile['profile_uid'] = $update;
} elseif ($a->profile['profile_uid'] == local_user()) {
$a->profile['uid'] = $update;
} elseif ($a->profile['uid'] == local_user()) {
Nav::setSelected('home');
}
$remote_contact = Session::getRemoteContactID($a->profile['profile_uid']);
$is_owner = local_user() == $a->profile['profile_uid'];
$last_updated_key = "profile:" . $a->profile['profile_uid'] . ":" . local_user() . ":" . $remote_contact;
$remote_contact = Session::getRemoteContactID($a->profile['uid']);
$is_owner = local_user() == $a->profile['uid'];
$last_updated_key = "profile:" . $a->profile['uid'] . ":" . local_user() . ":" . $remote_contact;
if (!empty($a->profile['hidewall']) && !$is_owner && !$remote_contact) {
notice(DI::l10n()->t('Access to this profile has been restricted.') . EOL);
@ -182,16 +182,16 @@ class Profile extends BaseModule
return $o;
}
$o .= Widget::commonFriendsVisitor($a->profile['profile_uid']);
$o .= Widget::commonFriendsVisitor($a->profile['uid']);
$commpage = $a->profile['page-flags'] == User::PAGE_FLAGS_COMMUNITY;
$commvisitor = $commpage && $remote_contact;
DI::page()['aside'] .= Widget::postedByYear(DI::baseUrl()->get(true) . '/profile/' . $a->profile['nickname'], $a->profile['profile_uid'] ?? 0, true);
DI::page()['aside'] .= Widget::postedByYear(DI::baseUrl()->get(true) . '/profile/' . $a->profile['nickname'], $a->profile['uid'] ?? 0, true);
DI::page()['aside'] .= Widget::categories(DI::baseUrl()->get(true) . '/profile/' . $a->profile['nickname'], XML::escape($category));
DI::page()['aside'] .= Widget::tagCloud();
if (Security::canWriteToUserWall($a->profile['profile_uid'])) {
if (Security::canWriteToUserWall($a->profile['uid'])) {
$x = [
'is_owner' => $is_owner,
'allow_location' => ($is_owner || $commvisitor) && $a->profile['allow_location'],
@ -206,7 +206,7 @@ class Profile extends BaseModule
'acl' => $is_owner ? ACL::getFullSelectorHTML(DI::page(), $a->user, true) : '',
'bang' => '',
'visitor' => $is_owner || $commvisitor ? 'block' : 'none',
'profile_uid' => $a->profile['profile_uid'],
'profile_uid' => $a->profile['uid'],
];
$o .= status_editor($a, $x);
@ -214,7 +214,7 @@ class Profile extends BaseModule
}
// Get permissions SQL - if $remote_contact is true, our remote user has been pre-verified and we already have fetched his/her groups
$sql_extra = Item::getPermissionsSQLByUserId($a->profile['profile_uid']);
$sql_extra = Item::getPermissionsSQLByUserId($a->profile['uid']);
$sql_extra2 = '';
$last_updated_array = Session::get('last_updated', []);
@ -246,7 +246,7 @@ class Profile extends BaseModule
$sql_extra4
$sql_extra
ORDER BY `item`.`received` DESC",
$a->profile['profile_uid'],
$a->profile['uid'],
GRAVITY_ACTIVITY
);
@ -260,12 +260,12 @@ class Profile extends BaseModule
if (!empty($category)) {
$sql_post_table = sprintf("INNER JOIN (SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d ORDER BY `tid` DESC) AS `term` ON `item`.`id` = `term`.`oid` ",
DBA::escape(Strings::protectSprintf($category)), intval(TERM_OBJ_POST), intval(TERM_CATEGORY), intval($a->profile['profile_uid']));
DBA::escape(Strings::protectSprintf($category)), intval(TERM_OBJ_POST), intval(TERM_CATEGORY), intval($a->profile['uid']));
}
if (!empty($hashtags)) {
$sql_post_table .= sprintf("INNER JOIN (SELECT `oid` FROM `term` WHERE `term` = '%s' AND `otype` = %d AND `type` = %d AND `uid` = %d ORDER BY `tid` DESC) AS `term` ON `item`.`id` = `term`.`oid` ",
DBA::escape(Strings::protectSprintf($hashtags)), intval(TERM_OBJ_POST), intval(TERM_HASHTAG), intval($a->profile['profile_uid']));
DBA::escape(Strings::protectSprintf($hashtags)), intval(TERM_OBJ_POST), intval(TERM_HASHTAG), intval($a->profile['uid']));
}
if (!empty($datequery)) {
@ -277,7 +277,7 @@ class Profile extends BaseModule
// Does the profile page belong to a forum?
// If not then we can improve the performance with an additional condition
$condition = ['uid' => $a->profile['profile_uid'], 'page-flags' => [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_PRVGROUP]];
$condition = ['uid' => $a->profile['uid'], 'page-flags' => [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_PRVGROUP]];
if (!DBA::exists('user', $condition)) {
$sql_extra3 = sprintf(" AND `thread`.`contact-id` = %d ", intval(intval($a->profile['contact_id'])));
} else {
@ -321,7 +321,7 @@ class Profile extends BaseModule
$sql_extra2
ORDER BY `thread`.`received` DESC
$pager_sql",
$a->profile['profile_uid']
$a->profile['uid']
);
}
@ -344,13 +344,13 @@ class Profile extends BaseModule
$items = DBA::toArray($items_stmt);
if ($pager->getStart() == 0 && !empty($a->profile['profile_uid'])) {
$pinned_items = Item::selectPinned($a->profile['profile_uid'], ['uri', 'pinned'], ['true' . $sql_extra]);
if ($pager->getStart() == 0 && !empty($a->profile['uid'])) {
$pinned_items = Item::selectPinned($a->profile['uid'], ['uri', 'pinned'], ['true' . $sql_extra]);
$pinned = Item::inArray($pinned_items);
$items = array_merge($items, $pinned);
}
$o .= conversation($a, $items, $pager, 'profile', $update, false, 'pinned_received', $a->profile['profile_uid']);
$o .= conversation($a, $items, $pager, 'profile', $update, false, 'pinned_received', $a->profile['uid']);
if (!$update) {
$o .= $pager->renderMinimal(count($items));

View file

@ -40,7 +40,7 @@ class Contacts extends BaseModule
Profile::load($a, $nickname);
$is_owner = $a->profile['profile_uid'] == local_user();
$is_owner = $a->profile['uid'] == local_user();
// tabs
$o = Profile::getTabs($a, 'contacts', $is_owner, $nickname);

View file

@ -87,7 +87,7 @@ class Register extends BaseModule
if (DI::config()->get('system', 'publish_all')) {
$profile_publish = '<input type="hidden" name="profile_publish_reg" value="1" />';
} else {
$publish_tpl = Renderer::getMarkupTemplate('profile_publish.tpl');
$publish_tpl = Renderer::getMarkupTemplate('profile/publish.tpl');
$profile_publish = Renderer::replaceMacros($publish_tpl, [
'$instance' => 'reg',
'$pubdesc' => DI::l10n()->t('Include your profile in member directory?'),

View file

@ -0,0 +1,203 @@
<?php
namespace Friendica\Module\Settings\Profile\Photo;
use Friendica\Core\Renderer;
use Friendica\Core\Session;
use Friendica\Core\Worker;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Photo;
use Friendica\Module\BaseSettingsModule;
use Friendica\Network\HTTPException;
class Crop extends BaseSettingsModule
{
public static function post(array $parameters = [])
{
if (!Session::isAuthenticated()) {
return;
}
$photo_prefix = $parameters['guid'];
$resource_id = $photo_prefix;
$scale = 0;
if (substr($photo_prefix, -2, 1) == '-') {
list($resource_id, $scale) = explode('-', $photo_prefix);
}
self::checkFormSecurityTokenRedirectOnError('settings/profile/photo/crop/' . $photo_prefix, 'settings_profile_photo_crop');
$action = $_POST['action'] ?? 'crop';
// Image selection origin is top left
$selectionX = intval($_POST['xstart'] ?? 0);
$selectionY = intval($_POST['ystart'] ?? 0);
$selectionW = intval($_POST['width'] ?? 0);
$selectionH = intval($_POST['height'] ?? 0);
$path = 'profile/' . DI::app()->user['nickname'];
$base_image = Photo::selectFirst([], ['resource-id' => $resource_id, 'uid' => local_user(), 'scale' => $scale]);
if (DBA::isResult($base_image)) {
$Image = Photo::getImageForPhoto($base_image);
if ($Image->isValid()) {
// If setting for the default profile, unset the profile photo flag from any other photos I own
DBA::update('photo', ['profile' => 0], ['uid' => local_user()]);
// Normalizing expected square crop parameters
$selectionW = $selectionH = min($selectionW, $selectionH);
$imageIsSquare = $Image->getWidth() === $Image->getHeight();
$selectionIsFullImage = $selectionX === 0 && $selectionY === 0 && $selectionW === $Image->getWidth() && $selectionH === $Image->getHeight();
// Bypassed UI with a rectangle image, we force a square cropped image
if (!$imageIsSquare && $action == 'skip') {
$selectionX = $selectionY = 0;
$selectionW = $selectionH = min($Image->getWidth(), $Image->getHeight());
$action = 'crop';
}
// Selective crop if it was asked and the selection isn't the full image
if ($action == 'crop'
&& !($imageIsSquare && !$selectionIsFullImage)
) {
$Image->crop(300, $selectionX, $selectionY, $selectionW, $selectionH);
$resource_id = Photo::newResource();
} else {
$Image->scaleDown(300);
}
$r = Photo::store(
$Image,
local_user(),
0,
$resource_id,
$base_image['filename'],
DI::l10n()->t('Profile Photos'),
4,
1
);
if ($r === false) {
notice(DI::l10n()->t('Image size reduction [%s] failed.', '300'));
}
$Image->scaleDown(80);
$r = Photo::store(
$Image,
local_user(),
0,
$resource_id,
$base_image['filename'],
DI::l10n()->t('Profile Photos'),
5,
1
);
if ($r === false) {
notice(DI::l10n()->t('Image size reduction [%s] failed.', '80'));
}
$Image->scaleDown(48);
$r = Photo::store(
$Image,
local_user(),
0,
$resource_id,
$base_image['filename'],
DI::l10n()->t('Profile Photos'),
6,
1
);
if ($r === false) {
notice(DI::l10n()->t('Image size reduction [%s] failed.', '48'));
}
Contact::updateSelfFromUserID(local_user(), true);
info(DI::l10n()->t('Shift-reload the page or clear browser cache if the new photo does not display immediately.'));
// Update global directory in background
if ($path && strlen(DI::config()->get('system', 'directory'))) {
Worker::add(PRIORITY_LOW, 'Directory', DI::baseUrl()->get() . '/' . $path);
}
Worker::add(PRIORITY_LOW, 'ProfileUpdate', local_user());
} else {
notice(DI::l10n()->t('Unable to process image'));
}
}
DI::baseUrl()->redirect($path);
}
public static function content(array $parameters = [])
{
if (!Session::isAuthenticated()) {
throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.'));
}
parent::content();
$resource_id = $parameters['guid'];
$photos = Photo::selectToArray([], ['resource-id' => $resource_id, 'uid' => local_user()], ['order' => ['scale' => false]]);
if (!DBA::isResult($photos)) {
throw new HTTPException\NotFoundException(DI::l10n()->t('Photo not found.'));
}
$havescale = false;
$smallest = 0;
foreach ($photos as $photo) {
$smallest = $photo['scale'] == 1 ? 1 : $smallest;
$havescale = $havescale || $photo['scale'] == 5;
}
// set an already uloaded photo as profile photo
// if photo is in 'Profile Photos', change it in db
if ($photos[0]['album'] == DI::l10n()->t('Profile Photos') && $havescale) {
Photo::update(['profile' => false], ['uid' => local_user()]);
Photo::update(['profile' => true], ['resource-id' => $resource_id, 'uid' => local_user()]);
Contact::updateSelfFromUserID(local_user(), true);
// Update global directory in background
if (Session::get('my_url') && strlen(DI::config()->get('system', 'directory'))) {
Worker::add(PRIORITY_LOW, 'Directory', Session::get('my_url'));
}
notice(DI::l10n()->t('Profile picture successfully updated.'));
DI::baseUrl()->redirect('profile/' . DI::app()->user['nickname']);
}
$Image = Photo::getImageForPhoto($photos[0]);
$imagecrop = [
'resource-id' => $resource_id,
'scale' => $smallest,
'ext' => $Image->getExt(),
];
$isSquare = $Image->getWidth() === $Image->getHeight();
DI::page()['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/profile/photo/crop_head.tpl'), []);
$filename = $imagecrop['resource-id'] . '-' . $imagecrop['scale'] . '.' . $imagecrop['ext'];
$tpl = Renderer::getMarkupTemplate('settings/profile/photo/crop.tpl');
$o = Renderer::replaceMacros($tpl, [
'$filename' => $filename,
'$resource' => $imagecrop['resource-id'] . '-' . $imagecrop['scale'],
'$image_url' => DI::baseUrl() . '/photo/' . $filename,
'$title' => DI::l10n()->t('Crop Image'),
'$desc' => DI::l10n()->t('Please adjust the image cropping for optimum viewing.'),
'$form_security_token' => self::getFormSecurityToken('settings_profile_photo_crop'),
'$skip' => $isSquare ? DI::l10n()->t('Use Image As Is') : '',
'$crop' => DI::l10n()->t('Crop Image'),
]);
return $o;
}
}

View file

@ -0,0 +1,128 @@
<?php
namespace Friendica\Module\Settings\Profile\Photo;
use Friendica\App\Arguments;
use Friendica\Core\Renderer;
use Friendica\Core\Session;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Photo;
use Friendica\Module\BaseSettingsModule;
use Friendica\Network\HTTPException;
use Friendica\Object\Image;
use Friendica\Util\Images;
use Friendica\Util\Strings;
class Index extends BaseSettingsModule
{
public static function post(array $parameters = [])
{
if (!Session::isAuthenticated()) {
return;
}
self::checkFormSecurityTokenRedirectOnError('/settings/profile/photo', 'settings_profile_photo');
if (empty($_FILES['userfile'])) {
notice(DI::l10n()->t('Missing uploaded image.'));
return;
}
$src = $_FILES['userfile']['tmp_name'];
$filename = basename($_FILES['userfile']['name']);
$filesize = intval($_FILES['userfile']['size']);
$filetype = $_FILES['userfile']['type'];
if ($filetype == '') {
$filetype = Images::guessType($filename);
}
$maximagesize = DI::config()->get('system', 'maximagesize', 0);
if ($maximagesize && $filesize > $maximagesize) {
notice(DI::l10n()->t('Image exceeds size limit of %s', Strings::formatBytes($maximagesize)));
@unlink($src);
return;
}
$imagedata = @file_get_contents($src);
$Image = new Image($imagedata, $filetype);
if (!$Image->isValid()) {
notice(DI::l10n()->t('Unable to process image.'));
@unlink($src);
return;
}
$Image->orient($src);
@unlink($src);
$max_length = DI::config()->get('system', 'max_image_length', 0);
if ($max_length > 0) {
$Image->scaleDown($max_length);
}
$width = $Image->getWidth();
$height = $Image->getHeight();
if ($width < 175 || $height < 175) {
$Image->scaleUp(300);
$width = $Image->getWidth();
$height = $Image->getHeight();
}
$resource_id = Photo::newResource();
$filename = '';
if (Photo::store($Image, local_user(), 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 0)) {
info(DI::l10n()->t('Image uploaded successfully.'));
} else {
notice(DI::l10n()->t('Image upload failed.'));
}
if ($width > 640 || $height > 640) {
$Image->scaleDown(640);
if (!Photo::store($Image, local_user(), 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 1)) {
notice(DI::l10n()->t('Image size reduction [%s] failed.', '640'));
}
}
DI::baseUrl()->redirect('settings/profile/photo/crop/' . $resource_id);
}
public static function content(array $parameters = [])
{
if (!Session::isAuthenticated()) {
throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.'));
}
parent::content();
$args = DI::args();
$newuser = $args->get($args->getArgc() - 1) === 'new';
$contact = Contact::selectFirst(['avatar'], ['uid' => local_user(), 'self' => true]);
$tpl = Renderer::getMarkupTemplate('settings/profile/photo/index.tpl');
$o = Renderer::replaceMacros($tpl, [
'$title' => DI::l10n()->t('Profile Picture Settings'),
'$current_picture' => DI::l10n()->t('Current Profile Picture'),
'$upload_picture' => DI::l10n()->t('Upload Profile Picture'),
'$lbl_upfile' => DI::l10n()->t('Upload Picture:'),
'$submit' => DI::l10n()->t('Upload'),
'$avatar' => $contact['avatar'],
'$form_security_token' => self::getFormSecurityToken('settings_profile_photo'),
'$select' => sprintf('%s %s',
DI::l10n()->t('or'),
($newuser) ?
'<a href="' . DI::baseUrl() . '">' . DI::l10n()->t('skip this step') . '</a>'
: '<a href="' . DI::baseUrl() . '/photos/' . DI::app()->user['nickname'] . '">'
. DI::l10n()->t('select a photo from your photo albums') . '</a>'
),
]);
return $o;
}
}

View file

@ -50,7 +50,13 @@ class HTTPException
$message = $explanation[$e->getCode()] ?? '';
}
return ['$title' => $title, '$message' => $message, '$back' => DI::l10n()->t('Go back')];
$vars = ['$title' => $title, '$message' => $message, '$back' => DI::l10n()->t('Go back')];
if (is_site_admin()) {
$vars['$trace'] = $e->getTraceAsString();
}
return $vars;
}
/**

View file

@ -61,7 +61,7 @@ class Thread
$this->writable = true;
break;
case 'profile':
$this->profile_owner = $a->profile['profile_uid'];
$this->profile_owner = $a->profile['uid'];
$this->writable = Security::canWriteToUserWall($this->profile_owner);
break;
case 'display':

View file

@ -0,0 +1,154 @@
<?php
namespace Friendica\Repository;
use Friendica\BaseModel;
use Friendica\BaseRepository;
use Friendica\Collection;
use Friendica\Core\L10n;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model;
use Friendica\Model\Group;
use Friendica\Network\HTTPException;
class PermissionSet extends BaseRepository
{
protected static $table_name = 'permissionset';
protected static $model_class = Model\PermissionSet::class;
protected static $collection_class = Collection\PermissionSets::class;
/**
* @param array $data
* @return Model\PermissionSet
*/
protected function create(array $data)
{
return new Model\PermissionSet($this->dba, $this->logger, $data);
}
/**
* @param array $condition
* @return Model\PermissionSet
* @throws \Friendica\Network\HTTPException\NotFoundException
*/
public function selectFirst(array $condition)
{
if (isset($condition['id']) && !$condition['id']) {
return $this->create([
'id' => 0,
'uid' => $condition['uid'] ?? 0,
'allow_cid' => '',
'allow_gid' => '',
'deny_cid' => '',
'deny_gid' => '',
]);
}
return parent::selectFirst($condition);
}
/**
* @param array $condition
* @param array $params
* @return Collection\PermissionSets
* @throws \Exception
*/
public function select(array $condition = [], array $params = [])
{
return parent::select($condition, $params);
}
/**
* @param array $condition
* @param array $params
* @param int|null $max_id
* @param int|null $since_id
* @param int $limit
* @return Collection\PermissionSets
* @throws \Exception
*/
public function selectByBoundaries(array $condition = [], array $params = [], int $max_id = null, int $since_id = null, int $limit = self::LIMIT)
{
return parent::selectByBoundaries($condition, $params, $max_id, $since_id, $limit);
}
/**
* Fetch the id of a given permission set. Generate a new one when needed
*
* @param int $uid
* @param string|null $allow_cid Allowed contact IDs - empty = everyone
* @param string|null $allow_gid Allowed group IDs - empty = everyone
* @param string|null $deny_cid Disallowed contact IDs - empty = no one
* @param string|null $deny_gid Disallowed group IDs - empty = no one
* @return int id
* @throws \Exception
*/
public function getIdFromACL(
int $uid,
string $allow_cid = null,
string $allow_gid = null,
string $deny_cid = null,
string $deny_gid = null
) {
$ACLFormatter = DI::aclFormatter();
$allow_cid = $ACLFormatter->sanitize($allow_cid);
$allow_gid = $ACLFormatter->sanitize($allow_gid);
$deny_cid = $ACLFormatter->sanitize($deny_cid);
$deny_gid = $ACLFormatter->sanitize($deny_gid);
// Public permission
if (!$allow_cid && !$allow_gid && !$deny_cid && !$deny_gid) {
return 0;
}
$condition = [
'uid' => $uid,
'allow_cid' => $allow_cid,
'allow_gid' => $allow_gid,
'deny_cid' => $deny_cid,
'deny_gid' => $deny_gid
];
try {
$permissionset = $this->selectFirst($condition);
} catch(HTTPException\NotFoundException $exception) {
$permissionset = $this->insert($condition);
}
return $permissionset->id;
}
/**
* Returns a permission set collection for a given contact
*
* @param integer $contact_id Contact id of the visitor
* @param integer $uid User id whom the items belong, used for ownership check.
*
* @return Collection\PermissionSets
* @throws \Exception
*/
public function selectByContactId($contact_id, $uid)
{
$groups = [];
if (DBA::exists('contact', ['id' => $contact_id, 'uid' => $uid, 'blocked' => false])) {
$groups = Group::getIdsByContactId($contact_id);
}
$group_str = '<<>>'; // should be impossible to match
foreach ($groups as $group_id) {
$group_str .= '|<' . preg_quote($group_id) . '>';
}
$contact_str = '<' . $contact_id . '>';
$condition = ["`uid` = ? AND (NOT (`deny_cid` REGEXP ? OR deny_gid REGEXP ?)
AND (allow_cid REGEXP ? OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))",
$uid, $contact_str, $group_str, $contact_str, $group_str];
return $this->select($condition);
}
}

View file

@ -12,30 +12,57 @@ final class ACLFormatter
/**
* Turn user/group ACLs stored as angle bracketed text into arrays
*
* @param string|null $ids A angle-bracketed list of IDs
* @param string|null $acl_string A angle-bracketed list of IDs
*
* @return array The array based on the IDs (empty in case there is no list)
*/
public function expand(string $ids = null)
public function expand(string $acl_string = null)
{
// In case there is no ID list, return empty array (=> no ACL set)
if (!isset($ids)) {
if (!isset($acl_string)) {
return [];
}
// turn string array of angle-bracketed elements into numeric array
// e.g. "<1><2><3>" => array(1,2,3);
preg_match_all('/<(' . Group::FOLLOWERS . '|'. Group::MUTUALS . '|[0-9]+)>/', $ids, $matches, PREG_PATTERN_ORDER);
preg_match_all('/<(' . Group::FOLLOWERS . '|'. Group::MUTUALS . '|[0-9]+)>/', $acl_string, $matches, PREG_PATTERN_ORDER);
return $matches[1];
}
/**
* Takes an arbitrary ACL string and sanitizes it for storage
*
* @param string|null $acl_string
* @return string
*/
public function sanitize(string $acl_string = null)
{
if (empty($acl_string)) {
return '';
}
$cleaned_list = trim($acl_string, '<>');
if (empty($cleaned_list)) {
return '';
}
$elements = explode('><', $cleaned_list);
sort($elements);
array_walk($elements, [$this, 'sanitizeItem']);
return implode('', $elements);
}
/**
* Wrap ACL elements in angle brackets for storage
*
* @param string $item The item to sanitise
*/
private function sanitize(string &$item) {
private function sanitizeItem(string &$item) {
// The item is an ACL int value
if (intval($item)) {
$item = '<' . intval(Strings::escapeTags(trim($item))) . '>';
@ -70,7 +97,7 @@ final class ACLFormatter
}
if (is_array($item)) {
array_walk($item, [$this, 'sanitize']);
array_walk($item, [$this, 'sanitizeItem']);
$return = implode('', $item);
}
return $return;

View file

@ -131,12 +131,15 @@ class Temporal
if ($dob < '0000-01-01') {
$value = '';
$age = 0;
} elseif ($dob < '0001-00-00') {
$value = substr($dob, 5);
$age = 0;
} else {
$value = DateTimeFormat::utc(($year > 1000) ? $dob : '1000-' . $month . '-' . $day, 'Y-m-d');
$value = DateTimeFormat::utc($dob, 'Y-m-d');
$age = self::getAgeByTimezone($value, $timezone);
}
$age = (intval($value) ? self::getAgeByTimezone($value, $timezone, $timezone) : "");
$tpl = Renderer::getMarkupTemplate("field_input.tpl");
$o = Renderer::replaceMacros($tpl,
[
@ -144,7 +147,7 @@ class Temporal
'dob',
DI::l10n()->t('Birthday:'),
$value,
intval($age) > 0 ? DI::l10n()->t('Age: ') . $age : "",
intval($age) > 0 ? DI::l10n()->t('Age: ') . DI::l10n()->tt('%d year old', '%d years old', $age) : '',
'',
'placeholder="' . DI::l10n()->t('YYYY-MM-DD or MM-DD') . '"'
]
@ -339,48 +342,31 @@ class Temporal
/**
* Returns timezone correct age in years.
*
* Returns the age in years, given a date of birth, the timezone of the person
* whose date of birth is provided, and the timezone of the person viewing the
* result.
*
* Why? Bear with me. Let's say I live in Mittagong, Australia, and my birthday
* is on New Year's. You live in San Bruno, California.
* When exactly are you going to see my age increase?
*
* A: 5:00 AM Dec 31 San Bruno time. That's precisely when I start celebrating
* and become a year older. If you wish me happy birthday on January 1
* (San Bruno time), you'll be a day late.
* Returns the age in years, given a date of birth and the timezone of the person
* whose date of birth is provided.
*
* @param string $dob Date of Birth
* @param string $owner_tz (optional) Timezone of the person of interest
* @param string $viewer_tz (optional) Timezone of the person viewing
*
* @return int Age in years
* @throws \Exception
*/
public static function getAgeByTimezone($dob, $owner_tz = '', $viewer_tz = '')
public static function getAgeByTimezone($dob, $owner_tz = '')
{
if (!intval($dob)) {
return 0;
}
if (!$owner_tz) {
$owner_tz = date_default_timezone_get();
}
if (!$viewer_tz) {
$viewer_tz = date_default_timezone_get();
}
$birthdate = DateTimeFormat::convert($dob . ' 00:00:00+00:00', $owner_tz, 'UTC', 'Y-m-d');
list($year, $month, $day) = explode("-", $birthdate);
$year_diff = DateTimeFormat::timezoneNow($viewer_tz, 'Y') - $year;
$curr_month = DateTimeFormat::timezoneNow($viewer_tz, 'm');
$curr_day = DateTimeFormat::timezoneNow($viewer_tz, 'd');
$birthdate = new DateTime($dob . ' 00:00:00', new DateTimeZone($owner_tz));
$currentDate = new DateTime('now', new DateTimeZone('UTC'));
if (($curr_month < $month) || (($curr_month == $month) && ($curr_day < $day))) {
$year_diff--;
}
$interval = $birthdate->diff($currentDate);
return $year_diff;
return $interval->format('%y');
}
/**

View file

@ -228,6 +228,10 @@ return [
'/verify' => [Module\Settings\TwoFactor\Verify::class, [R::GET, R::POST]],
],
'/delegation[/{action}/{user_id}]' => [Module\Settings\Delegation::class, [R::GET, R::POST]],
'/profile' => [
'/photo[/new]' => [Module\Settings\Profile\Photo\Index::class, [R::GET, R::POST]],
'/photo/crop/{guid}' => [Module\Settings\Profile\Photo\Crop::class, [R::GET, R::POST]],
],
'/userexport[/{action}]' => [Module\Settings\UserExport::class, [R::GET, R::POST]],
],

View file

@ -15,15 +15,15 @@ use Friendica\Util\DateTimeFormat;
use Friendica\Worker\Delivery;
/**
* update.php - automatic post-databse structure change updates
*
* update.php - automatic system update
*
* This function is responsible for doing post update changes to the data
* (not the structure) in the database.
* These functions are responsible for doing critical post update changes to the data (not the structure) in the database.
*
* Database structure changes are done in static/dbstructure.config.php
*
* If there is a need for a post process to a structure change, update this file
* For non-critical database migrations, please add a method in the Database\PostUpdate class
*
* If there is a need for a post update to a structure change, update this file
* by adding a new function at the end with the number of the new DB_UPDATE_VERSION.
*
* The numbered script in this file has to be exactly like the DB_UPDATE_VERSION

View file

@ -1,11 +1,11 @@
<div id="acl-wrapper">
<div class="panel-group" id="visibility-accordion" role="tablist" aria-multiselectable="true">
<div class="panel-group" id="visibility-accordion-{{$input_group_id}}" role="tablist" aria-multiselectable="true">
<div class="panel panel-success">
<label class="panel-heading{{if $visibility != 'public'}} collapsed{{/if}}" id="visibility-public-heading" aria-expanded="{{if $visibility == 'public'}}true{{else}}false{{/if}}">
<input type="radio" name="visibility" id="visibility-public" value="public" tabindex="14" {{if $visibility == 'public'}}checked{{/if}}>
<label class="panel-heading{{if $visibility != 'public'}} collapsed{{/if}}" id="visibility-public-heading-{{$input_group_id}}" aria-expanded="{{if $visibility == 'public'}}true{{else}}false{{/if}}">
<input type="radio" name="{{$input_names.visibility}}" id="visibility-public-{{$input_group_id}}" value="public" tabindex="14" {{if $visibility == 'public'}}checked{{/if}}>
<i class="fa fa-globe"></i> {{$public_title}}
</label>
<fieldset id="visibility-public-panel" class="panel-collapse collapse{{if $visibility == 'public'}} in{{/if}}" role="tabpanel" aria-labelledby="visibility-public-heading" {{if $visibility != 'public'}}disabled{{/if}}>
<fieldset id="visibility-public-panel-{{$input_group_id}}" class="panel-collapse collapse{{if $visibility == 'public'}} in{{/if}}" role="tabpanel" aria-labelledby="visibility-public-heading-{{$input_group_id}}" {{if $visibility != 'public'}}disabled{{/if}}>
<div class="panel-body">
<p>{{$public_desc}}</p>
{{if $for_federation}}
@ -39,26 +39,26 @@
</fieldset>
</div>
<div class="panel panel-info">
<label class="panel-heading{{if $visibility != 'custom'}} collapsed{{/if}}" id="visibility-custom-heading" aria-expanded="{{if $visibility == 'custom'}}true{{else}}false{{/if}}">
<input type="radio" name="visibility" id="visibility-custom" value="custom" tabindex="15" {{if $visibility == 'custom'}}checked{{/if}}>
<label class="panel-heading{{if $visibility != 'custom'}} collapsed{{/if}}" id="visibility-custom-heading-{{$input_group_id}}" aria-expanded="{{if $visibility == 'custom'}}true{{else}}false{{/if}}">
<input type="radio" name="{{$input_names.visibility}}" id="visibility-custom-{{$input_group_id}}" value="custom" tabindex="15" {{if $visibility == 'custom'}}checked{{/if}}>
<i class="fa fa-lock"></i> {{$custom_title}}
</label>
<fieldset id="visibility-custom-panel" class="panel-collapse collapse{{if $visibility == 'custom'}} in{{/if}}" role="tabpanel" aria-labelledby="visibility-custom-heading" {{if $visibility != 'custom'}}disabled{{/if}}>
<input type="hidden" name="group_allow" value="{{$group_allow}}"/>
<input type="hidden" name="contact_allow" value="{{$contact_allow}}"/>
<input type="hidden" name="group_deny" value="{{$group_deny}}"/>
<input type="hidden" name="contact_deny" value="{{$contact_deny}}"/>
<fieldset id="visibility-custom-panel-{{$input_group_id}}" class="panel-collapse collapse{{if $visibility == 'custom'}} in{{/if}}" role="tabpanel" aria-labelledby="visibility-custom-heading-{{$input_group_id}}" {{if $visibility != 'custom'}}disabled{{/if}}>
<input type="hidden" name="{{$input_names.group_allow}}" value="{{$group_allow}}"/>
<input type="hidden" name="{{$input_names.contact_allow}}" value="{{$contact_allow}}"/>
<input type="hidden" name="{{$input_names.group_deny}}" value="{{$group_deny}}"/>
<input type="hidden" name="{{$input_names.contact_deny}}" value="{{$contact_deny}}"/>
<div class="panel-body">
<p>{{$custom_desc}}</p>
<div class="form-group">
<label for="acl_allow">{{$allow_label}}</label>
<input type="text" class="form-control input-lg" id="acl_allow">
<label for="acl_allow-{{$input_group_id}}">{{$allow_label}}</label>
<input type="text" class="form-control input-lg" id="acl_allow-{{$input_group_id}}">
</div>
<div class="form-group">
<label for="acl_deny">{{$deny_label}}</label>
<input type="text" class="form-control input-lg" id="acl_deny">
<label for="acl_deny-{{$input_group_id}}">{{$deny_label}}</label>
<input type="text" class="form-control input-lg" id="acl_deny-{{$input_group_id}}">
</div>
</div>
</fieldset>
@ -68,29 +68,29 @@
{{if $for_federation}}
<div class="form-group">
<label for="profile-jot-email" id="profile-jot-email-label">{{$emailcc}}</label>
<input type="text" name="emailcc" id="profile-jot-email" class="form-control" title="{{$emtitle}}" />
<label for="profile-jot-email" id="profile-jot-email-label-{{$input_group_id}}">{{$emailcc}}</label>
<input type="text" name="{{$input_names.emailcc}}" id="profile-jot-email-{{$input_group_id}}" class="form-control" title="{{$emtitle}}" />
</div>
<div id="profile-jot-email-end"></div>
<div id="profile-jot-email-end-{{$input_group_id}}"></div>
{{/if}}
</div>
<script type="text/javascript">
$(function() {
let $acl_allow_input = $('#acl_allow');
let $contact_allow_input = $('[name=contact_allow]');
let $group_allow_input = $('[name=group_allow]');
let $acl_deny_input = $('#acl_deny');
let $contact_deny_input = $('[name=contact_deny]');
let $group_deny_input = $('[name=group_deny]');
let $visibility_public_panel = $('#visibility-public-panel');
let $visibility_custom_panel = $('#visibility-custom-panel');
let $visibility_public_radio = $('#visibility-public');
let $visibility_custom_radio = $('#visibility-custom');
let $acl_allow_input = $('#acl_allow-{{$input_group_id}}');
let $contact_allow_input = $('[name="{{$input_names.contact_allow}}"]');
let $group_allow_input = $('[name="{{$input_names.group_allow}}"]');
let $acl_deny_input = $('#acl_deny-{{$input_group_id}}');
let $contact_deny_input = $('[name="{{$input_names.contact_deny}}"]');
let $group_deny_input = $('[name="{{$input_names.group_deny}}"]');
let $visibility_public_panel = $('#visibility-public-panel-{{$input_group_id}}');
let $visibility_custom_panel = $('#visibility-custom-panel-{{$input_group_id}}');
let $visibility_public_radio = $('#visibility-public-{{$input_group_id}}');
let $visibility_custom_radio = $('#visibility-custom-{{$input_group_id}}');
// Frio specific
if ($.fn.collapse) {
$visibility_public_panel.collapse({parent: '#visibility-accordion', toggle: false});
$visibility_custom_panel.collapse({parent: '#visibility-accordion', toggle: false});
$visibility_public_panel.collapse({parent: '#visibility-accordion-{{$input_group_id}}', toggle: false});
$visibility_custom_panel.collapse({parent: '#visibility-accordion-{{$input_group_id}}', toggle: false});
}
$visibility_public_radio.on('change', function (e) {
@ -101,13 +101,13 @@
$visibility_public_panel.prop('disabled', false);
$visibility_custom_panel.prop('disabled', true);
$('.profile-jot-net input[type=checkbox]').each(function() {
$('#visibility-public-panel-{{$input_group_id}} .profile-jot-net input[type=checkbox]').each(function() {
// Restores checkbox state if it had been saved
if ($(this).attr('data-checked') !== undefined) {
$(this).prop('checked', $(this).attr('data-checked') === 'true');
}
});
$('.profile-jot-net input').attr('disabled', false);
$('#visibility-public-panel-{{$input_group_id}} .profile-jot-net input').attr('disabled', false);
});
$visibility_custom_radio.on('change', function(e) {
@ -118,13 +118,13 @@
$visibility_public_panel.prop('disabled', true);
$visibility_custom_panel.prop('disabled', false);
$('.profile-jot-net input[type=checkbox]').each(function() {
$('#visibility-public-panel-{{$input_group_id}} .profile-jot-net input[type=checkbox]').each(function() {
// Saves current checkbox state
$(this)
.attr('data-checked', $(this).prop('checked'))
.prop('checked', false);
});
$('.profile-jot-net input').attr('disabled', 'disabled');
$('#visibility-public-panel-{{$input_group_id}} .profile-jot-net input').attr('disabled', 'disabled');
});
// Custom visibility tags inputs
@ -233,9 +233,11 @@
// Anti-duplicate callback + acl fields value generation
$acl_allow_input.on('itemAdded', function (event) {
$acl_allow_input.on('itemAdded itemRemoved', function (event) {
if (event.type === 'itemAdded') {
// Removes duplicate in the opposite acl box
$acl_deny_input.tagsinput('remove', event.item);
}
// Update the real acl field
$group_allow_input.val('');
@ -249,9 +251,11 @@
});
});
$acl_deny_input.on('itemAdded', function (event) {
$acl_deny_input.on('itemAdded itemRemoved', function (event) {
if (event.type === 'itemAdded') {
// Removes duplicate in the opposite acl box
$acl_allow_input.tagsinput('remove', event.item);
}
// Update the real acl field
$group_deny_input.val('');

View file

@ -1,54 +0,0 @@
<h1>{{$title}}</h1>
<p id="cropimage-desc">
{{$desc nofilter}}
</p>
<div id="cropimage-wrapper">
<img src="{{$image_url}}" id="croppa" class="imgCrop" alt="{{$title}}" />
</div>
<div id="cropimage-preview-wrapper" >
<div id="previewWrap" class="crop-preview"></div>
</div>
<script type="text/javascript" language="javascript">
var image = document.getElementById('croppa');
var cropper = new Cropper(image, {
aspectRatio: 1 / 1,
viewMode: 1,
preview: '#profile-photo-wrapper, .crop-preview',
crop: function(e) {
$( '#x1' ).val(e.detail.x);
$( '#y1' ).val(e.detail.y);
$( '#x2' ).val(e.detail.x + e.detail.width);
$( '#y2' ).val(e.detail.y + e.detail.height);
$( '#width' ).val(e.detail.scaleX);
$( '#height' ).val(e.detail.scaleY);
},
ready: function() {
// Add the "crop-preview" class to the preview element ("profile-photo-wrapper").
var cwrapper = document.getElementById("profile-photo-wrapper");
cwrapper.classList.add("crop-preview");
}
});
</script>
<form action="profile_photo/{{$resource}}" id="crop-image-form" method="post" />
<input type='hidden' name='form_security_token' value='{{$form_security_token}}'>
<input type='hidden' name='profile' value='{{$profile}}'>
<input type="hidden" name="cropfinal" value="1" />
<input type="hidden" name="xstart" id="x1" />
<input type="hidden" name="ystart" id="y1" />
<input type="hidden" name="xfinal" id="x2" />
<input type="hidden" name="yfinal" id="y2" />
<input type="hidden" name="height" id="height" />
<input type="hidden" name="width" id="width" />
<div id="crop-image-submit-wrapper" >
<input type="submit" name="submit" value="{{$done}}" />
</div>
</form>

View file

@ -2,5 +2,8 @@
<img class="hare" src="images/friendica-404_svg_flexy-o-hare.png"/>
<h1>{{$title}}</h1>
<p>{{$message}}</p>
{{if $trace}}
<pre>{{$trace nofilter}}</pre>
{{/if}}
<p><button type="button" onclick="window.history.back()" class="btn btn-primary">{{$back}}</button></p>
</div>

View file

@ -4,6 +4,9 @@
</head>
<body>
<h1>{{$title}}</h1>
<p>{{$description nofilter}}</p>
<p>{{$message nofilter}}</p>
{{if $trace}}
<pre>{{$trace nofilter}}</pre>
{{/if}}
</body>
</html>

View file

@ -1,27 +0,0 @@
<h1>{{$title}}</h1>
<form enctype="multipart/form-data" action="profile_photo" method="post">
<input type='hidden' name='form_security_token' value='{{$form_security_token}}'>
<div id="profile-photo-upload-wrapper">
<label id="profile-photo-upload-label" for="profile-photo-upload">{{$lbl_upfile}} </label>
<input name="userfile" type="file" id="profile-photo-upload" size="48" />
</div>
<label id="profile-photo-profiles-label" for="profile-photo-profiles">{{$lbl_profiles}} </label>
<select name="profile" id="profile-photo-profiles" />
{{foreach $profiles as $p}}
<option value="{{$p.id}}" {{if $p.default}}selected="selected"{{/if}}>{{$p.name}}</option>
{{/foreach}}
</select>
<div id="profile-photo-submit-wrapper">
<input type="submit" name="submit" id="profile-photo-submit" value="{{$submit}}">
</div>
</form>
<div id="profile-photo-link-select-wrapper">
{{$select nofilter}}
</div>

View file

@ -0,0 +1,55 @@
<div class="generic-page-wrapper">
<h1>{{$title}}</h1>
<p id="cropimage-desc">
{{$desc nofilter}}
</p>
<div id="cropimage-wrapper">
<p><img src="{{$image_url}}" id="croppa" class="imgCrop" alt="{{$title}}"></p>
</div>
<div id="cropimage-preview-wrapper" >
<div id="previewWrap" class="crop-preview"></div>
</div>
<form action="settings/profile/photo/crop/{{$resource}}" id="crop-image-form" method="post">
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
<input type="hidden" name="xstart" id="x1" />
<input type="hidden" name="ystart" id="y1" />
<input type="hidden" name="height" id="height" />
<input type="hidden" name="width" id="width" />
<div id="settings-profile-photo-crop-submit-wrapper" class="pull-right settings-submit-wrapper">
{{if $skip}}
<button type="submit" name="action" id="settings-profile-photo-crop-skip" class="btn" value="skip">{{$skip}}</button>
{{/if}}
<button type="submit" name="action" id="settings-profile-photo-crop-submit" class="btn btn-primary" value="crop">{{$crop}}</button>
</div>
<div class="clear"></div>
</form>
<script type="text/javascript" language="javascript">
var image = document.getElementById('croppa');
var cropper = new Cropper(image, {
aspectRatio: 1,
viewMode: 1,
preview: '#profile-photo-wrapper, .crop-preview',
crop: function(e) {
$('#x1').val(e.detail.x);
$('#y1').val(e.detail.y);
$('#width').val(e.detail.width);
$('#height').val(e.detail.height);
},
});
var skip_button = document.getElementById('settings-profile-photo-crop-skip');
skip_button.addEventListener('click', function() {
let image_data = cropper.getImageData();
cropper.setData({x: 0, y: 0, width: image_data.width, height: image_data.height});
})
</script>
</div>

View file

@ -0,0 +1,25 @@
<div class="generic-page-wrapper">
<h1>{{$title}}</h1>
<h2>{{$current_picture}}</h2>
<p><img src="{{$avatar}}" alt="{{$current_picture}}"/></p>
<h2>{{$upload_picture}}</h2>
<form enctype="multipart/form-data" action="settings/profile/photo" method="post">
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
<div id="profile-photo-upload-wrapper" class="form-group field input">
<label id="profile-photo-upload-label" for="profile-photo-upload">{{$lbl_upfile}} </label>
<input class="form-control" name="userfile" type="file" id="profile-photo-upload" size="48">
<div class="clear"></div>
</div>
<div id="profile-photo-submit-wrapper" class="pull-right settings-submit-wrapper">
<button type="submit" name="submit" id="profile-photo-submit" class="btn btn-primary" value="{{$submit}}">{{$submit}}</button>
</div>
<div class="clear"></div>
</form>
<p id="profile-photo-link-select-wrapper">
{{$select nofilter}}
</p>
</div>

View file

@ -22,7 +22,7 @@
<h4>{{$profile nofilter}}</h4>
<ul>
<li>
<a target="newmember" href="profile_photo">{{$profile_photo_link}}</a><br />
<a target="newmember" href="settings/profile/photo">{{$profile_photo_link}}</a><br />
{{$profile_photo_txt nofilter}}
</li>
<li>

View file

@ -2288,7 +2288,7 @@ ul.dropdown-menu li:hover {
* PAGES
*********/
.generic-page-wrapper, .profile_photo-content-wrapper, .videos-content-wrapper,
.generic-page-wrapper, .videos-content-wrapper,
.suggest-content-wrapper, .common-content-wrapper, .help-content-wrapper,
.allfriends-content-wrapper, .match-content-wrapper, .dirfind-content-wrapper,
.delegation-content-wrapper, .notes-content-wrapper,
@ -2850,21 +2850,15 @@ ul li:hover .contact-wrapper .contact-action-link:hover {
#profile-listing-new-link-wrapper {
margin-bottom: 20px;
}
.panel-group-settings {
margin-left: -15px;
margin-right: -15px;
}
.panel-group-settings > .panel,
.panel-group-settings > form > .panel {
padding-left: 15px;
padding-right: 15px;
padding-left: 10px;
padding-right: 10px;
}
.profiles-content-wrapper #profile-photo-upload-section {
#profile-photo-upload-section {
display: none;
margin-left: -15px;
margin-right: -15px;
margin-top: 15px;
padding: 15px;
padding: 10px;
}
#profile-photo-upload-close {
font-size: 14px;
@ -3537,7 +3531,7 @@ section .profile-match-wrapper {
right: 10px;
}
.generic-page-wrapper, .profile_photo-content-wrapper, .videos-content-wrapper, .suggest-content-wrapper, .common-content-wrapper, .help-content-wrapper, .allfriends-content-wrapper, .match-content-wrapper, .dirfind-content-wrapper, .directory-content-wrapper, .delegation-content-wrapper, .notes-content-wrapper, .message-content-wrapper, .apps-content-wrapper, #adminpage, .delegate-content-wrapper, .uexport-content-wrapper, .dfrn_request-content-wrapper, .friendica-content-wrapper, .credits-content-wrapper, .nogroup-content-wrapper, .profperm-content-wrapper, .invite-content-wrapper, .tos-content-wrapper, .fsuggest-content-wrapper {
.generic-page-wrapper, .videos-content-wrapper, .suggest-content-wrapper, .common-content-wrapper, .help-content-wrapper, .allfriends-content-wrapper, .match-content-wrapper, .dirfind-content-wrapper, .directory-content-wrapper, .delegation-content-wrapper, .notes-content-wrapper, .message-content-wrapper, .apps-content-wrapper, #adminpage, .delegate-content-wrapper, .uexport-content-wrapper, .dfrn_request-content-wrapper, .friendica-content-wrapper, .credits-content-wrapper, .nogroup-content-wrapper, .profperm-content-wrapper, .invite-content-wrapper, .tos-content-wrapper, .fsuggest-content-wrapper {
border-radius: 0;
padding: 10px;
}

View file

@ -6,21 +6,10 @@
<div class="action">
<a class="icon s16 edit ttright" href="#" rel="#profiles-menu" title="{{$profile.edit.3}}"><span>{{$profile.edit.1}}</span></a>
<ul id="profiles-menu" class="menu-popup">
{{if $profile.menu.entries}}
{{foreach $profile.menu.entries as $e}}
<li>
<a href="profiles/{{$e.id}}"><img src='{{$e.photo}}'>{{$e.profile_name}}</a>
</li>
{{/foreach}}
{{else}}
<li>
<a href="{{$profile.edit.0}}">{{$profile.edit.1}}</a>
</li>
{{/if}}
<li><a href="profile_photo" >{{$profile.menu.chg_photo}}</a></li>
{{if $profile.menu.cr_new }}
<li><a href="profiles/new" id="profile-listing-new-link">{{$profile.menu.cr_new}}</a></li>
{{/if}}
<li><a href="settings/profile/photo" >{{$profile.menu.chg_photo}}</a></li>
</ul>
</div>
{{/if}}

View file

@ -145,7 +145,7 @@ function vier_community_info()
$tpl = Renderer::getMarkupTemplate('ch_directory_item.tpl');
$r = q("SELECT `profile`.*, `profile`.`uid` AS `profile_uid`, `user`.`nickname`
$r = q("SELECT `profile`.*, `user`.`nickname`
FROM `profile` LEFT JOIN `user` ON `user`.`uid` = `profile`.`uid`
WHERE `is-default` = 1 $publish AND `user`.`blocked` = 0 $order LIMIT %d , %d ",
0,
@ -157,7 +157,7 @@ function vier_community_info()
$aside['$lastusers_items'] = [];
foreach ($r as $rr) {
$profile_link = 'profile/' . ((strlen($rr['nickname'])) ? $rr['nickname'] : $rr['profile_uid']);
$profile_link = 'profile/' . ((strlen($rr['nickname'])) ? $rr['nickname'] : $rr['uid']);
$entry = Renderer::replaceMacros($tpl, [
'$id' => $rr['id'],
'$profile_link' => $profile_link,