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 3acabc84..aa742cfc 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 3363c5de..0d967202 100644 --- a/Web/di.yml +++ b/Web/di.yml @@ -25,6 +25,7 @@ services: - openvk\Web\Presenters\VKAPIPresenter - openvk\Web\Presenters\PollPresenter - openvk\Web\Presenters\BannedLinkPresenter + - openvk\Web\Presenters\LinksPresenter - openvk\Web\Models\Repositories\Users - openvk\Web\Models\Repositories\Posts - openvk\Web\Models\Repositories\Polls @@ -44,6 +45,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 d3dcbc83..88c0e5da 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -211,6 +211,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; diff --git a/locales/en.strings b/locales/en.strings index 6da1f451..ca4e6824 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -790,6 +790,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 b25a3dc1..425ede4f 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -725,6 +725,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" = "Открытые";