diff --git a/include/conversation.php b/include/conversation.php index 264fa6ceb4..ed273a52ce 100644 --- a/include/conversation.php +++ b/include/conversation.php @@ -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 = '
' . "\r\n" - . "\r\n"; } } diff --git a/mod/cal.php b/mod/cal.php index cf0e498d56..e6570018b7 100644 --- a/mod/cal.php +++ b/mod/cal.php @@ -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); diff --git a/mod/display.php b/mod/display.php index f06b98dd4a..b5edafc5f9 100644 --- a/mod/display.php +++ b/mod/display.php @@ -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.')); diff --git a/mod/photos.php b/mod/photos.php index c2fb825696..5b8d22b655 100644 --- a/mod/photos.php +++ b/mod/photos.php @@ -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 ( diff --git a/mod/profile_photo.php b/mod/profile_photo.php deleted file mode 100644 index d34c78363c..0000000000 --- a/mod/profile_photo.php +++ /dev/null @@ -1,324 +0,0 @@ -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) ? '' . DI::l10n()->t('skip this step') . '' : '' . DI::l10n()->t('select a photo from your photo albums') . '') - ]); - - 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; -} diff --git a/mod/profiles.php b/mod/profiles.php index 551ae54f9b..53da8510c4 100644 --- a/mod/profiles.php +++ b/mod/profiles.php @@ -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')], diff --git a/src/App/Authentication.php b/src/App/Authentication.php index c0408a8111..2e1a823c71 100644 --- a/src/App/Authentication.php +++ b/src/App/Authentication.php @@ -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'])); } diff --git a/src/BaseCollection.php b/src/BaseCollection.php index 8bad3b8d28..4d5803d585 100644 --- a/src/BaseCollection.php +++ b/src/BaseCollection.php @@ -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)); + } } diff --git a/src/BaseModel.php b/src/BaseModel.php index 055f9c4a19..b2dc7eedaf 100644 --- a/src/BaseModel.php +++ b/src/BaseModel.php @@ -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'); + } + } } diff --git a/src/BaseModule.php b/src/BaseModule.php index 2250f22bbc..96ec2e9f1f 100644 --- a/src/BaseModule.php +++ b/src/BaseModule.php @@ -61,8 +61,7 @@ abstract class BaseModule */ public static function post(array $parameters = []) { - // $a = self::getApp(); - // $a->internalRedirect('module'); + // DI::baseurl()->redirect('module'); } /** diff --git a/src/BaseRepository.php b/src/BaseRepository.php index 9f43d8fe14..c0bcab18f9 100644 --- a/src/BaseRepository.php +++ b/src/BaseRepository.php @@ -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,11 +136,13 @@ abstract class BaseRepository extends BaseFactory { $return = $this->dba->insert(static::$table_name, $fields); - if ($return) { - $fields['id'] = $this->dba->lastInsertId(); - $return = $this->create($fields); + 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']); + } } diff --git a/src/Collection/PermissionSets.php b/src/Collection/PermissionSets.php new file mode 100644 index 0000000000..7511d046fd --- /dev/null +++ b/src/Collection/PermissionSets.php @@ -0,0 +1,10 @@ + 0) { - $o .= $forumlist; - return $o; - } + return $o; } /** diff --git a/src/Content/Widget.php b/src/Content/Widget.php index a38b9a3051..8fa09ee8a2 100644 --- a/src/Content/Widget.php +++ b/src/Content/Widget.php @@ -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 ''; diff --git a/src/Core/ACL.php b/src/Core/ACL.php index 09d19321ae..cfe46e7706 100644 --- a/src/Core/ACL.php +++ b/src/Core/ACL.php @@ -228,19 +228,34 @@ class ACL /** * Returns the ACL list of contacts for a given user id * - * @param int $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 @@ -295,26 +310,38 @@ class ACL /** * Return the full jot ACL selector HTML * - * @param Page $page - * @param array $user User array - * @param bool $for_federation - * @param array $default_permissions Static defaults permission array: - * [ + * @param Page $page + * @param array $user User array + * @param bool $for_federation + * @param array $default_permissions Static defaults permission array: + * [ * 'allow_cid' => [], * 'allow_gid' => [], * 'deny_cid' => [], * '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; diff --git a/src/Core/Session.php b/src/Core/Session.php index d0c8581d31..0af4d344b1 100644 --- a/src/Core/Session.php +++ b/src/Core/Session.php @@ -61,7 +61,7 @@ class Session $session = DI::session(); if (empty($session->get('remote')[$uid])) { - return false; + return 0; } return $session->get('remote')[$uid]; diff --git a/src/DI.php b/src/DI.php index 22c81079b9..e6d9c341b7 100644 --- a/src/DI.php +++ b/src/DI.php @@ -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 // diff --git a/src/Database/Database.php b/src/Database/Database.php index 8a893ab935..14e186a72f 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -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)) { - unset($fields[$fieldname]); - } else { - $do_update = true; - } + if (isset($fields[$fieldname]) && !is_null($content) && ($fields[$fieldname] == $content)) { + unset($fields[$fieldname]); } } - if (!$do_update || (count($fields) == 0)) { + if (count($fields) == 0) { return true; } diff --git a/src/Database/PostUpdate.php b/src/Database/PostUpdate.php index 64737f604f..7fd28419ea 100644 --- a/src/Database/PostUpdate.php +++ b/src/Database/PostUpdate.php @@ -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,14 +207,20 @@ class PostUpdate } if (empty($item['psid'])) { - $item['psid'] = PermissionSet::fetchIDForPost($item); - } else { - $item['allow_cid'] = null; - $item['allow_gid'] = null; - $item['deny_cid'] = null; - $item['deny_gid'] = null; + $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')) { $item['post-type'] = Item::PT_PERSONAL_NOTE; diff --git a/src/Model/Group.php b/src/Model/Group.php index c38e78000b..199ff1ebd7 100644 --- a/src/Model/Group.php +++ b/src/Model/Group.php @@ -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; } diff --git a/src/Model/Item.php b/src/Model/Item.php index 9f2f45e74a..38557c52ce 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -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); diff --git a/src/Model/PermissionSet.php b/src/Model/PermissionSet.php index d0e256d153..9bf2642c8b 100644 --- a/src/Model/PermissionSet.php +++ b/src/Model/PermissionSet.php @@ -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'); } } diff --git a/src/Model/Profile.php b/src/Model/Profile.php index 341727ceef..b993562cec 100644 --- a/src/Model/Profile.php +++ b/src/Model/Profile.php @@ -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'), diff --git a/src/Module/Profile.php b/src/Module/Profile.php index 97c108841c..e3ae7b3f57 100644 --- a/src/Module/Profile.php +++ b/src/Module/Profile.php @@ -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'] .= '' . "\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)); diff --git a/src/Module/Profile/Contacts.php b/src/Module/Profile/Contacts.php index 09691100aa..1a21df0ece 100644 --- a/src/Module/Profile/Contacts.php +++ b/src/Module/Profile/Contacts.php @@ -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); diff --git a/src/Module/Register.php b/src/Module/Register.php index 8a92b25088..98af06543d 100644 --- a/src/Module/Register.php +++ b/src/Module/Register.php @@ -87,7 +87,7 @@ class Register extends BaseModule if (DI::config()->get('system', 'publish_all')) { $profile_publish = ''; } 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?'), diff --git a/src/Module/Settings/Profile/Photo/Crop.php b/src/Module/Settings/Profile/Photo/Crop.php new file mode 100644 index 0000000000..738e3fcb2a --- /dev/null +++ b/src/Module/Settings/Profile/Photo/Crop.php @@ -0,0 +1,203 @@ +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; + } +} diff --git a/src/Module/Settings/Profile/Photo/Index.php b/src/Module/Settings/Profile/Photo/Index.php new file mode 100644 index 0000000000..9d2648abd8 --- /dev/null +++ b/src/Module/Settings/Profile/Photo/Index.php @@ -0,0 +1,128 @@ +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) ? + '' . DI::l10n()->t('skip this step') . '' + : '' + . DI::l10n()->t('select a photo from your photo albums') . '' + ), + ]); + + return $o; + } +} diff --git a/src/Module/Special/HTTPException.php b/src/Module/Special/HTTPException.php index d2c8503859..55357d09c6 100644 --- a/src/Module/Special/HTTPException.php +++ b/src/Module/Special/HTTPException.php @@ -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; } /** diff --git a/src/Object/Thread.php b/src/Object/Thread.php index f88dee2c41..0d0def0612 100644 --- a/src/Object/Thread.php +++ b/src/Object/Thread.php @@ -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': diff --git a/src/Repository/PermissionSet.php b/src/Repository/PermissionSet.php new file mode 100644 index 0000000000..906dd716e6 --- /dev/null +++ b/src/Repository/PermissionSet.php @@ -0,0 +1,154 @@ +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); + } +} diff --git a/src/Util/ACLFormatter.php b/src/Util/ACLFormatter.php index a7d851508d..d79a73298b 100644 --- a/src/Util/ACLFormatter.php +++ b/src/Util/ACLFormatter.php @@ -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; diff --git a/src/Util/Temporal.php b/src/Util/Temporal.php index ecbfcae543..ef27b54b2d 100644 --- a/src/Util/Temporal.php +++ b/src/Util/Temporal.php @@ -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'); } /** diff --git a/static/routes.config.php b/static/routes.config.php index 115b8f5f6d..05f2599561 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -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]], ], diff --git a/update.php b/update.php index 4d12730fab..22ed4ae9e3 100644 --- a/update.php +++ b/update.php @@ -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 diff --git a/view/templates/acl_selector.tpl b/view/templates/acl_selector.tpl index a9da4f6118..babedd6c02 100644 --- a/view/templates/acl_selector.tpl +++ b/view/templates/acl_selector.tpl @@ -1,11 +1,11 @@
-
+
-
-
- -
- - - - - - - - - - - -
- -
- -
diff --git a/view/templates/exception.tpl b/view/templates/exception.tpl index 6c26168908..04e9f82c02 100644 --- a/view/templates/exception.tpl +++ b/view/templates/exception.tpl @@ -2,5 +2,8 @@

{{$title}}

{{$message}}

+{{if $trace}} +
{{$trace nofilter}}
+{{/if}}

diff --git a/view/templates/http_status.tpl b/view/templates/http_status.tpl index 6b366d6f04..a9c094c4b2 100644 --- a/view/templates/http_status.tpl +++ b/view/templates/http_status.tpl @@ -4,6 +4,9 @@

{{$title}}

-

{{$description nofilter}}

+

{{$message nofilter}}

+ {{if $trace}} +
{{$trace nofilter}}
+ {{/if}} diff --git a/view/templates/profile_advanced.tpl b/view/templates/profile/advanced.tpl similarity index 100% rename from view/templates/profile_advanced.tpl rename to view/templates/profile/advanced.tpl diff --git a/view/templates/profile_publish.tpl b/view/templates/profile/publish.tpl similarity index 100% rename from view/templates/profile_publish.tpl rename to view/templates/profile/publish.tpl diff --git a/view/templates/profile_vcard.tpl b/view/templates/profile/vcard.tpl similarity index 100% rename from view/templates/profile_vcard.tpl rename to view/templates/profile/vcard.tpl diff --git a/view/templates/profile_photo.tpl b/view/templates/profile_photo.tpl deleted file mode 100644 index e7c6b89608..0000000000 --- a/view/templates/profile_photo.tpl +++ /dev/null @@ -1,27 +0,0 @@ - -

{{$title}}

- -
- - -
- - -
- - - - -
- -
- -
- - \ No newline at end of file diff --git a/view/templates/settings/profile/photo/crop.tpl b/view/templates/settings/profile/photo/crop.tpl new file mode 100644 index 0000000000..679c9d1350 --- /dev/null +++ b/view/templates/settings/profile/photo/crop.tpl @@ -0,0 +1,55 @@ +
+

{{$title}}

+

+ {{$desc nofilter}} +

+ +
+

{{$title}}

+
+ +
+
+
+ +
+ + + + + + + +
+ {{if $skip}} + + {{/if}} + +
+ +
+
+ + +
diff --git a/view/templates/crophead.tpl b/view/templates/settings/profile/photo/crop_head.tpl similarity index 100% rename from view/templates/crophead.tpl rename to view/templates/settings/profile/photo/crop_head.tpl diff --git a/view/templates/settings/profile/photo/index.tpl b/view/templates/settings/profile/photo/index.tpl new file mode 100644 index 0000000000..a819e11307 --- /dev/null +++ b/view/templates/settings/profile/photo/index.tpl @@ -0,0 +1,25 @@ +
+

{{$title}}

+ +

{{$current_picture}}

+

{{$current_picture}}

+

{{$upload_picture}}

+
+ + +
+ + +
+
+ +
+ +
+
+
+ + +
\ No newline at end of file diff --git a/view/templates/welcome.tpl b/view/templates/welcome.tpl index 9f343efbbb..4e0ae7754d 100644 --- a/view/templates/welcome.tpl +++ b/view/templates/welcome.tpl @@ -22,7 +22,7 @@

{{$profile nofilter}}