From 1b71a3ad25f4d4451dd8eb74a91ea614fd376f5c Mon Sep 17 00:00:00 2001 From: Maxim Leshchenko Date: Sun, 25 Sep 2022 17:47:25 +0200 Subject: [PATCH 1/2] Users, Groups: Add a block with links This is similar to the links that are in the original VK, but unlike the original, links can be not only the group but also the page (Linktree moment). --- Web/Models/Entities/Link.php | 117 ++++++++++++++++++++ Web/Models/Repositories/Links.php | 38 +++++++ Web/Presenters/GroupPresenter.php | 4 +- Web/Presenters/LinksPresenter.php | 137 ++++++++++++++++++++++++ Web/Presenters/UserPresenter.php | 4 +- Web/Presenters/templates/Group/View.xml | 26 +++++ Web/Presenters/templates/Links/Edit.xml | 71 ++++++++++++ Web/Presenters/templates/Links/List.xml | 42 ++++++++ Web/Presenters/templates/User/View.xml | 26 +++++ Web/di.yml | 2 + Web/routes.yml | 8 ++ install/sqls/00034-links.sql | 15 +++ 12 files changed, 488 insertions(+), 2 deletions(-) create mode 100644 Web/Models/Entities/Link.php create mode 100644 Web/Models/Repositories/Links.php create mode 100644 Web/Presenters/LinksPresenter.php create mode 100644 Web/Presenters/templates/Links/Edit.xml create mode 100644 Web/Presenters/templates/Links/List.xml create mode 100644 install/sqls/00034-links.sql diff --git a/Web/Models/Entities/Link.php b/Web/Models/Entities/Link.php new file mode 100644 index 00000000..97e3ac8a --- /dev/null +++ b/Web/Models/Entities/Link.php @@ -0,0 +1,117 @@ +getRecord()->id; + } + + function getOwner(): RowModel + { + $ownerId = (int) $this->getRecord()->owner; + + if($ownerId > 0) + return (new Users)->get($ownerId); + else + return (new Clubs)->get($ownerId * -1); + } + + function getTitle(): string + { + return $this->getRecord()->title; + } + + function getDescription(): ?string + { + return $this->getRecord()->description; + } + + function getDescriptionOrDomain(): string + { + $description = $this->getDescription(); + + if(is_null($description)) + return $this->getDomain(); + else + return $description; + } + + function getUrl(): string + { + return $this->getRecord()->url; + } + + function getIconUrl(): string + { + $serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"]; + if(is_null($this->getRecord()->icon_hash)) + return "$serverUrl/assets/packages/static/openvk/img/camera_200.png"; + + $hash = $this->getRecord()->icon_hash; + switch(OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["mode"]) { + default: + case "default": + case "basic": + return "$serverUrl/blob_" . substr($hash, 0, 2) . "/$hash" . "_link_icon.png"; + case "accelerated": + return "$serverUrl/openvk-datastore/$hash" . "_link_icon.png"; + case "server": + $settings = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["server"]; + return ( + $settings->protocol ?? ovk_scheme() . + "://" . $settings->host . + $settings->path . + substr($hash, 0, 2) . "/$hash" . "_link_icon.png" + ); + } + } + + function setIcon(array $file): int + { + if($file["error"] !== UPLOAD_ERR_OK) + return -1; + + try { + $image = Image::fromFile($file["tmp_name"]); + } catch (UnknownImageFileException $e) { + return -2; + } + + $hash = hash_file("adler32", $file["tmp_name"]); + if(!is_dir($this->getIconsDir() . substr($hash, 0, 2))) + if(!mkdir($this->getIconsDir() . substr($hash, 0, 2))) + return -3; + + $image->resize(140, 140, Image::STRETCH); + $image->save($this->getIconsDir() . substr($hash, 0, 2) . "/$hash" . "_link_icon.png"); + + $this->stateChanges("icon_hash", $hash); + + return 0; + } + + function getDomain(): string + { + return parse_url($this->getUrl(), PHP_URL_HOST); + } + + use Traits\TOwnable; +} diff --git a/Web/Models/Repositories/Links.php b/Web/Models/Repositories/Links.php new file mode 100644 index 00000000..eed992cd --- /dev/null +++ b/Web/Models/Repositories/Links.php @@ -0,0 +1,38 @@ +context = DatabaseConnection::i()->getContext(); + $this->links = $this->context->table("links"); + } + + function get(int $id): ?Link + { + $link = $this->links->get($id); + if(!$link) return NULL; + + return new Link($link); + } + + function getByOwnerId(int $ownerId, int $page = 1, ?int $perPage = NULL): \Traversable + { + $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; + $links = $this->links->where("owner", $ownerId)->page($page, $perPage); + + foreach($links as $link) + yield new Link($link); + } + + function getCountByOwnerId(int $id): int + { + return sizeof($this->links->where("owner", $id)); + } +} diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index 00d74c2e..93fc464c 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -2,7 +2,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{Club, Photo}; use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification; -use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics}; +use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Links}; use Chandler\Security\Authenticator; final class GroupPresenter extends OpenVKPresenter @@ -27,6 +27,8 @@ final class GroupPresenter extends OpenVKPresenter $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); $this->template->topics = (new Topics)->getLastTopics($club, 3); $this->template->topicsCount = (new Topics)->getClubTopicsCount($club); + $this->template->links = (new Links)->getByOwnerId($club->getId() * -1, 1, 5); + $this->template->linksCount = (new Links)->getCountByOwnerId($club->getId() * -1); $this->template->club = $club; } diff --git a/Web/Presenters/LinksPresenter.php b/Web/Presenters/LinksPresenter.php new file mode 100644 index 00000000..0de0730b --- /dev/null +++ b/Web/Presenters/LinksPresenter.php @@ -0,0 +1,137 @@ +links = $links; + + parent::__construct(); + } + + function renderList(int $ownerId): void + { + $owner = ($ownerId < 0 ? (new Clubs) : (new Users))->get(abs($ownerId)); + if(!$owner) + $this->notFound(); + + $this->template->owner = $owner; + $this->template->ownerId = $ownerId; + $page = (int) ($this->queryParam("p") ?? 1); + + $this->template->links = $this->links->getByOwnerId($ownerId, $page); + $this->template->count = $this->links->getCountByOwnerId($ownerId); + + $this->template->paginatorConf = (object) [ + "count" => $this->template->count, + "page" => $page, + "amount" => NULL, + "perPage" => OPENVK_DEFAULT_PER_PAGE, + ]; + } + + function renderCreate(int $ownerId): void + { + $this->assertUserLoggedIn(); + + $owner = ($ownerId < 0 ? (new Clubs) : (new Users))->get(abs($ownerId)); + if(!$owner) + $this->notFound(); + + if($ownerId < 0 ? !$owner->canBeModifiedBy($this->user->identity) : $owner->getId() !== $this->user->id) + $this->notFound(); + + $this->template->_template = "Links/Edit.xml"; + $this->template->create = true; + $this->template->owner = $owner; + $this->template->ownerId = $ownerId; + } + + function renderEdit(int $ownerId, int $id): void + { + $this->assertUserLoggedIn(); + + $owner = ($ownerId < 0 ? (new Clubs) : (new Users))->get(abs($ownerId)); + if(!$owner) + $this->notFound(); + + $link = $this->links->get($id); + if(!$link && $id !== 0) // If the link ID is 0, consider the request as link creation + $this->notFound(); + + if($ownerId < 0 ? !$owner->canBeModifiedBy($this->user->identity) : $owner->getId() !== $this->user->id) + $this->notFound(); + + if($_SERVER["REQUEST_METHOD"] === "POST") { + $this->willExecuteWriteAction(); + + $create = $id === 0; + $title = $this->postParam("title"); + $description = $this->postParam("description"); + $url = $this->postParam("url"); + $url = (!parse_url($url, PHP_URL_SCHEME) ? "https://" : "") . $url; + + if(!$title || !$url) + $this->flashFail("err", tr($create ? "failed_to_create_link" : "failed_to_change_link"), tr("not_all_data_entered")); + + if(!filter_var($url, FILTER_VALIDATE_URL)) + $this->flashFail("err", tr($create ? "failed_to_create_link" : "failed_to_change_link"), tr("wrong_address")); + + if($create) + $link = new Link; + + $link->setOwner($ownerId); + $link->setTitle(ovk_proc_strtr($title, 127)); + $link->setDescription($description === "" ? NULL : ovk_proc_strtr($description, 127)); + $link->setUrl($url); + + if(isset($_FILES["icon"]) && $_FILES["icon"]["size"] > 0) { + if(($res = $link->setIcon($_FILES["icon"])) !== 0) + $this->flashFail("err", tr("unable_to_upload_icon"), tr("unable_to_upload_icon_desc", $res)); + } + + $link->save(); + + $this->flash("succ", tr("information_-1"), tr($create ? "link_created" : "link_changed")); + $this->redirect("/links" . $ownerId); + } + + if($id === 0) // But there is a separate handler for displaying page with the fields to create, so here we do not skip + $this->notFound(); + + $this->template->linkId = $link->getId(); + $this->template->title = $link->getTitle(); + $this->template->description = $link->getDescription(); + $this->template->url = $link->getUrl(); + $this->template->create = false; + $this->template->owner = $owner; + $this->template->ownerId = $ownerId; + $this->template->link = $link; + } + + function renderDelete(int $ownerId, int $id): void + { + $this->assertUserLoggedIn(); + + $owner = ($ownerId < 0 ? (new Clubs) : (new Users))->get(abs($ownerId)); + if(!$owner) + $this->notFound(); + + $link = $this->links->get($id); + if(!$link) + $this->notFound(); + + if(!$link->canBeModifiedBy($this->user->identity)) + $this->notFound(); + + $this->willExecuteWriteAction(); + $link->delete(false); + + $this->flashFail("succ", tr("information_-1"), tr("link_deleted")); + } +} diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index 48ebb660..01d63e5b 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -4,7 +4,7 @@ use openvk\Web\Util\Sms; use openvk\Web\Themes\Themepacks; use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification}; use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification}; -use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications}; +use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications, Links}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Util\Validator; use Chandler\Security\Authenticator; @@ -43,6 +43,8 @@ final class UserPresenter extends OpenVKPresenter $this->template->videosCount = (new Videos)->getUserVideosCount($user); $this->template->notes = (new Notes)->getUserNotes($user, 1, 4); $this->template->notesCount = (new Notes)->getUserNotesCount($user); + $this->template->links = (new Links)->getByOwnerId($user->getId(), 1, 5); + $this->template->linksCount = (new Links)->getCountByOwnerId($user->getId()); $this->template->user = $user; } diff --git a/Web/Presenters/templates/Group/View.xml b/Web/Presenters/templates/Group/View.xml index dc223af1..6b6d167c 100644 --- a/Web/Presenters/templates/Group/View.xml +++ b/Web/Presenters/templates/Group/View.xml @@ -191,6 +191,32 @@ +
+
+ {_links} +
+
+
+ {tr("links_count", $linksCount)} +
+ {_all_title} +
+
+
+
+
+ + + +
+
+ {$link->getTitle()} +
{$link->getDescriptionOrDomain()}
+
+
+
+
+
{_albums} diff --git a/Web/Presenters/templates/Links/Edit.xml b/Web/Presenters/templates/Links/Edit.xml new file mode 100644 index 00000000..e7452cb2 --- /dev/null +++ b/Web/Presenters/templates/Links/Edit.xml @@ -0,0 +1,71 @@ +{extends "../@layout.xml"} +{block title} + {if $create} + {_new_link} + {else} + {_edit_link} "{$title}" + {/if} +{/block} + +{block header} + {$owner->getCanonicalName()} + » + {_links} + » + {if $create}{_new_link}{else}{_edit_link}{/if} +{/block} + +{block content} +
+

{if $create}{_new_link}{else}{_edit_link}{/if}

+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ {_title}: + + +
+ {_address}: + + +
+ {_description}: + + +
+ {_icon}: + + +
+ + + + +
+ + +
+
+{/block} diff --git a/Web/Presenters/templates/Links/List.xml b/Web/Presenters/templates/Links/List.xml new file mode 100644 index 00000000..3e7c13a1 --- /dev/null +++ b/Web/Presenters/templates/Links/List.xml @@ -0,0 +1,42 @@ +{extends "../@listView.xml"} +{var $iterator = iterator_to_array($links)} +{var $page = $paginatorConf->page} + +{block title}{_links} {$owner->getCanonicalName()}{/block} + +{block header} + {$owner->getCanonicalName()} » {_links} + +
+ {_create_link} +
+{/block} + +{* BEGIN ELEMENTS DESCRIPTION *} + +{block link|strip|stripHtml} + /away.php?to={$x->getUrl()} +{/block} + +{block preview} + {$x->getTitle()} +{/block} + +{block name} + {$x->getTitle()} +{/block} + +{block description} + {$x->getDescriptionOrDomain()} +{/block} + +{block actions} + {if !is_null($thisUser) && $x->canBeModifiedBy($thisUser)} + + {_edit} + + + {_delete} + + {/if} +{/block} diff --git a/Web/Presenters/templates/User/View.xml b/Web/Presenters/templates/User/View.xml index fb38133d..5b5c66ea 100644 --- a/Web/Presenters/templates/User/View.xml +++ b/Web/Presenters/templates/User/View.xml @@ -229,6 +229,32 @@
+
+
+ {_links} +
+
+
+ {tr("links_count", $linksCount)} +
+ {_all_title} +
+
+
+
+
+ + + +
+
+ {$link->getTitle()} +
{$link->getDescriptionOrDomain()}
+
+
+
+
+
{_albums} diff --git a/Web/di.yml b/Web/di.yml index ec867809..b4faa6e5 100644 --- a/Web/di.yml +++ b/Web/di.yml @@ -24,6 +24,7 @@ services: - openvk\Web\Presenters\ThemepacksPresenter - openvk\Web\Presenters\VKAPIPresenter - openvk\Web\Presenters\BannedLinkPresenter + - openvk\Web\Presenters\LinksPresenter - openvk\Web\Models\Repositories\Users - openvk\Web\Models\Repositories\Posts - openvk\Web\Models\Repositories\Photos @@ -42,6 +43,7 @@ services: - openvk\Web\Models\Repositories\Gifts - openvk\Web\Models\Repositories\Topics - openvk\Web\Models\Repositories\Applications + - openvk\Web\Models\Repositories\Links - openvk\Web\Models\Repositories\ContentSearchRepository - openvk\Web\Models\Repositories\Aliases - openvk\Web\Models\Repositories\BannedLinks diff --git a/Web/routes.yml b/Web/routes.yml index d6f63a11..db1de156 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -207,6 +207,14 @@ routes: handler: "Topics->edit" - url: "/topic{num}_{num}/delete" handler: "Topics->delete" + - url: "/links{num}" + handler: "Links->list" + - url: "/links{num}/create" + handler: "Links->create" + - url: "/links{num}/edit{num}" + handler: "Links->edit" + - url: "/links{num}/delete{num}" + handler: "Links->delete" - url: "/audios{num}" handler: "Audios->app" - url: "/audios{num}.json" diff --git a/install/sqls/00034-links.sql b/install/sqls/00034-links.sql new file mode 100644 index 00000000..df0dca4d --- /dev/null +++ b/install/sqls/00034-links.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS `links` ( + `id` bigint(20) unsigned NOT NULL, + `owner` bigint(20) NOT NULL, + `title` varchar(128) COLLATE utf8mb4_unicode_520_ci NOT NULL, + `description` varchar(128) COLLATE utf8mb4_unicode_520_ci, + `url` varchar(128) COLLATE utf8mb4_unicode_520_ci NOT NULL, + `icon_hash` char(128) COLLATE utf8mb4_unicode_520_ci +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +ALTER TABLE `links` + ADD PRIMARY KEY (`id`), + ADD KEY `owner` (`owner`); + +ALTER TABLE `links` + MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT; From 363f2592171243db85191050e951f8438cb65689 Mon Sep 17 00:00:00 2001 From: Maxim Leshchenko Date: Sun, 13 Nov 2022 23:21:44 +0100 Subject: [PATCH 2/2] Links: I forgot to add translations :trollface: --- locales/en.strings | 25 +++++++++++++++++++++++++ locales/ru.strings | 27 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/locales/en.strings b/locales/en.strings index 240dd5d0..5466f85a 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -784,6 +784,31 @@ "app_err_note" = "Failed to attach a news note"; "app_err_note_desc" = "Make sure the link is correct and the note belongs to you."; +/* Links */ + +"links" = "Links"; +"create_link" = "Create link"; +"new_link" = "New link"; +"edit_link" = "Edit link"; +"icon" = "Icon"; + +"failed_to_create_link" = "Failed to create link"; +"failed_to_change_link" = "Failed to change link"; + +"unable_to_upload_icon" = "Unable to upload icon"; +"unable_to_upload_icon_desc" = "Icon too big or wrong: general error #$res."; + +"not_all_data_entered" = "Not all data has been entered."; +"wrong_address" = "Wrong address :("; + +"link_created" = "Link created."; +"link_changed" = "Link changed"; +"link_deleted" = "Link deleted."; + +"links_count_zero" = "No links"; +"links_count_one" = "One link"; +"links_count_other" = "$1 links"; + /* Support */ "support_opened" = "Opened"; diff --git a/locales/ru.strings b/locales/ru.strings index 6a7b8f46..cf60a811 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -831,6 +831,33 @@ "app_err_note" = "Не удалось прикрепить новостную заметку"; "app_err_note_desc" = "Убедитесь, что ссылка правильная и заметка принадлежит Вам."; +/* Links */ + +"links" = "Ссылки"; +"create_link" = "Создать ссылку"; +"new_link" = "Новая ссылка"; +"edit_link" = "Редактировать ссылку"; +"icon" = "Иконка"; + +"failed_to_create_link" = "Не удалось создать ссылку"; +"failed_to_change_link" = "Не удалось изменить ссылку"; + +"failed_to_upload_icon" = "Не удалось загрузить иконку"; +"failed_to_upload_icon_desc" = "Иконка слишком большая или кривая: ошибка общего характера №$res."; + +"not_all_data_entered" = "Не все данные введены."; +"wrong_address" = "Неправильный адрес :("; + +"link_created" = "Ссылка создана."; +"link_changed" = "Ссылка изменена"; +"link_deleted" = "Ссылка удалена."; + +"links_count_zero" = "Нет ссылок"; +"links_count_one" = "Одна ссылка"; +"links_count_few" = "$1 ссылки"; +"links_count_many" = "$1 ссылок"; +"links_count_other" = "$1 ссылок"; + /* Support */ "support_opened" = "Открытые";