This commit is contained in:
Maxim Leshchenko 2022-11-21 23:29:12 +03:00 committed by GitHub
commit 36689d1acb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 540 additions and 2 deletions

View file

@ -0,0 +1,117 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Nette\Utils\Image;
use Nette\Utils\UnknownImageFileException;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\RowModel;
class Link extends RowModel
{
protected $tableName = "links";
private function getIconsDir(): string
{
$uploadSettings = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"];
if($uploadSettings["mode"] === "server" && $uploadSettings["server"]["kind"] === "cdn")
return $uploadSettings["server"]["directory"];
else
return OPENVK_ROOT . "/storage/";
}
function getId(): int
{
return $this->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;
}

View file

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Link;
use Chandler\Database\DatabaseConnection;
class Links
{
private $context;
private $links;
function __construct()
{
$this->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));
}
}

View file

@ -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;
}

View file

@ -0,0 +1,137 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\Link;
use openvk\Web\Models\Repositories\{Links, Clubs, Users};
final class LinksPresenter extends OpenVKPresenter
{
private $links;
function __construct(Links $links)
{
$this->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"));
}
}

View file

@ -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;
}

View file

@ -191,6 +191,32 @@
</div>
</div>
</div>
<div n:if="$linksCount > 0 || ($thisUser && $club->canBeModifiedBy($thisUser))">
<div class="content_title_expanded" onclick="hidePanel(this, {$linksCount});">
{_links}
</div>
<div>
<div class="content_subtitle">
{tr("links_count", $linksCount)}
<div style="float: right;">
<a href="/links-{$club->getId()}">{_all_title}</a>
</div>
</div>
<div class="avatar-list">
<div class="avatar-list-item" n:foreach="$links as $link">
<div class="avatar">
<a href="/away.php?to={$link->getUrl()}">
<img height="32" class="ava" src="{$link->getIconUrl()}" />
</a>
</div>
<div class="info">
<a href="/away.php?to={$link->getUrl()}" class="title">{$link->getTitle()}</a>
<div class="subtitle">{$link->getDescriptionOrDomain()}</div>
</div>
</div>
</div>
</div>
</div>
<div n:if="$albumsCount > 0 || ($thisUser && $club->canBeModifiedBy($thisUser))">
<div class="content_title_expanded" onclick="hidePanel(this, {$albumsCount});">
{_albums}

View file

@ -0,0 +1,71 @@
{extends "../@layout.xml"}
{block title}
{if $create}
{_new_link}
{else}
{_edit_link} "{$title}"
{/if}
{/block}
{block header}
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
»
<a href="/links{$ownerId}">{_links}</a>
»
{if $create}{_new_link}{else}{_edit_link}{/if}
{/block}
{block content}
<div class="container_gray">
<h4>{if $create}{_new_link}{else}{_edit_link}{/if}</h4>
<form method="POST" action="/links{$ownerId}/edit{$linkId ?? 0}" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tbody>
<tr>
<td width="120" valign="top">
<span class="nobold">{_title}:</span>
</td>
<td>
<input type="text" name="title" value="{$title ?? ''}" />
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_address}:</span>
</td>
<td>
<input type="text" name="url" value="{$url ?? ''}" />
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_description}:</span>
</td>
<td>
<input type="text" name="description" value="{$description ?? ''}" />
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_icon}: </span>
</td>
<td>
<input type="file" name="icon" accept="image/*" />
</td>
</tr>
<tr>
<td width="120" valign="top">
</td>
<td>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_save}" class="button" />
</td>
</tr>
</tbody>
</table>
<input type="hidden" name="hash" value="{$csrfToken}" />
</form>
</div>
{/block}

View file

@ -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}
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a> » {_links}
<div n:if="!is_null($thisUser) && ($ownerId > 0 ? $ownerId === $thisUser->getId() : $owner->canBeModifiedBy($thisUser))" style="float: right;">
<a href="/links{$ownerId}/create">{_create_link}</a>
</div>
{/block}
{* BEGIN ELEMENTS DESCRIPTION *}
{block link|strip|stripHtml}
/away.php?to={$x->getUrl()}
{/block}
{block preview}
<img src="{$x->getIconUrl()}" alt="{$x->getTitle()}" width=75 />
{/block}
{block name}
{$x->getTitle()}
{/block}
{block description}
{$x->getDescriptionOrDomain()}
{/block}
{block actions}
{if !is_null($thisUser) && $x->canBeModifiedBy($thisUser)}
<a class="profile_link" href="/links{$ownerId}/edit{$x->getId()}">
{_edit}
</a>
<a class="profile_link" href="/links{$ownerId}/delete{$x->getId()}">
{_delete}
</a>
{/if}
{/block}

View file

@ -229,6 +229,32 @@
</div>
</div>
</div>
<div n:if="$linksCount > 0 || $thisUser != NULL && $user->getId() === $thisUser->getId()">
<div class="content_title_expanded" onclick="hidePanel(this, {$linksCount});">
{_links}
</div>
<div>
<div class="content_subtitle">
{tr("links_count", $linksCount)}
<div style="float: right;">
<a href="/links{$user->getId()}">{_all_title}</a>
</div>
</div>
<div class="avatar-list">
<div class="avatar-list-item" n:foreach="$links as $link">
<div class="avatar">
<a href="/away.php?to={$link->getUrl()}">
<img height="32" class="ava" src="{$link->getIconUrl()}" />
</a>
</div>
<div class="info">
<a href="/away.php?to={$link->getUrl()}" class="title">{$link->getTitle()}</a>
<div class="subtitle">{$link->getDescriptionOrDomain()}</div>
</div>
</div>
</div>
</div>
</div>
<div n:if="$albumsCount > 0 && $user->getPrivacyPermission('photos.read', $thisUser ?? NULL)">
<div class="content_title_expanded" onclick="hidePanel(this, {$albumsCount});">
{_albums}

View file

@ -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

View file

@ -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"

View file

@ -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;

View file

@ -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";

View file

@ -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" = "Открытые";