...
 
Commits (2)
<?php
/**
* settings assets
* @author edgebal
*/
namespace Minds\Controllers\api\v2\pro\settings;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Manager;
use Minds\Core\Pro\Assets\Manager as AssetsManager;
use Minds\Core\Session;
use Minds\Entities\User;
use Minds\Interfaces;
use Minds\Api\Factory;
use Zend\Diactoros\ServerRequest;
class assets implements Interfaces\Api
{
/** @var ServerRequest */
public $request;
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
*/
public function get($pages)
{
return Factory::response([]);
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
* @throws Exception
*/
public function post($pages)
{
$type = $pages[0] ?? null;
// Check and validate user
$user = Session::getLoggedinUser();
if (isset($pages[1]) && $pages[1]) {
if (!Session::isAdmin()) {
return Factory::response([
'status' => 'error',
'message' => 'You are not authorized',
]);
}
$user = new User($pages[1]);
}
// Check uploaded file
/** @var \Zend\Diactoros\UploadedFile[] $files */
$files = $this->request->getUploadedFiles();
if (!$files || !isset($files['file'])) {
return Factory::response([
'status' => 'error',
'message' => 'Missing file',
]);
}
$file = $files['file'];
if ($file->getError()) {
return Factory::response([
'status' => 'error',
'message' => sprintf('Error %s when uploading file', $files['file']->getError()),
]);
}
// Get Pro managers
/** @var Manager $manager */
$manager = Di::_()->get('Pro\Manager');
$manager
->setUser($user)
->setActor(Session::getLoggedinUser());
if (!$manager->isActive()) {
return Factory::response([
'status' => 'error',
'message' => 'You are not Pro',
]);
}
/** @var AssetsManager $assetsManager */
$assetsManager = Di::_()->get('Pro\Assets\Manager');
$assetsManager
->setType($type)
->setUser($user)
->setActor(Session::getLoggedinUser());
try {
$success = $assetsManager
->set($file);
if (!$success) {
throw new Exception(sprintf("Cannot save Pro %s asset", $type));
}
} catch (\Exception $e) {
return Factory::response([
'status' => 'error',
'message' => $e->getMessage(),
]);
}
return Factory::response([]);
}
/**
* Equivalent to HTTP PUT method
* @param array $pages
* @return mixed|null
*/
public function put($pages)
{
return Factory::response([]);
}
/**
* Equivalent to HTTP DELETE method
* @param array $pages
* @return mixed|null
*/
public function delete($pages)
{
return Factory::response([]);
}
}
<?php
/**
* pro
* @author edgebal
*/
namespace Minds\Controllers\fs\v1;
use Minds\Core\Pro\Assets\Asset;
use Minds\Interfaces;
class pro implements Interfaces\FS
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
* @throws \IOException
* @throws \InvalidParameterException
* @throws \Exception
*/
public function get($pages)
{
$asset = new Asset();
$asset
->setType($pages[1] ?? null)
->setUserGuid($pages[0] ?? null);
$file = $asset->getFile();
$file->open('read');
$contents = $file->read();
header(sprintf("Content-Type: %s", $asset->getMimeType()));
header(sprintf("Expires: %s", date('r', time() + 864000)));
header('Pragma: public');
header('Cache-Control: public');
echo $contents;
exit;
}
}
......@@ -49,6 +49,17 @@ class Manager
return $this->image->getImageBlob();
}
public function getPng()
{
if (!$this->image) {
throw new \Exception('Output was not generated');
}
$this->image->setImageFormat('png');
return $this->image->getImageBlob();
}
/**
* @param $value
* @return $this
......@@ -61,6 +72,14 @@ class Manager
return $this;
}
public function setImageFromBlob($blob, $fileName = null)
{
$this->image = new \Imagick();
$this->image->readImageBlob($blob, $fileName);
return $this;
}
/**
* @return $this
*/
......
<?php
/**
* Info
* @author edgebal
*/
namespace Minds\Core\Pro\Assets;
use ElggFile;
use Exception;
use Minds\Traits\MagicAttributes;
/**
* Class Asset
* @package Minds\Core\Pro\Assets
* @method string getType()
* @method int|string getUserGuid()
* @method Asset setUserGuid(int|string $userGuid)
*/
class Asset
{
use MagicAttributes;
/** @var string */
protected $type;
/** @var int|string */
protected $userGuid;
/** @var string[] */
const TYPES = ['logo', 'background'];
/**
* @param string $type
* @return Asset
* @throws Exception
*/
public function setType(string $type): Asset
{
if (!in_array($type, static::TYPES, true)) {
throw new Exception('Invalid Asset type');
}
$this->type = $type;
return $this;
}
/**
* @return string
* @throws Exception
*/
public function getExt(): string
{
switch ($this->type) {
case 'logo':
return 'png';
case 'background':
return 'jpg';
}
throw new Exception('Invalid Asset');
}
/**
* @return string
* @throws Exception
*/
public function getMimeType(): string
{
switch ($this->type) {
case 'logo':
return 'image/png';
case 'background':
return 'image/jpg';
}
throw new Exception('Invalid Asset');
}
/**
* @return ElggFile
* @throws Exception
*/
public function getFile(): ElggFile
{
$file = new ElggFile();
$file->owner_guid = $this->userGuid;
$file->setFilename(sprintf("pro/%s.%s", $this->type, $this->getExt()));
return $file;
}
}
<?php
/**
* Manager
* @author edgebal
*/
namespace Minds\Core\Pro\Assets;
use ElggFile;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Media\Imagick\Manager as ImageManager;
use Minds\Entities\User;
use Zend\Diactoros\UploadedFile;
class Manager
{
/** @var ImageManager */
protected $imageManager;
/** @var string */
protected $type;
/** @var User */
protected $user;
/** @var User */
protected $actor;
/**
* Manager constructor.
* @param ImageManager $imageManager
*/
public function __construct(
$imageManager = null
) {
$this->imageManager = $imageManager ?: Di::_()->get('Media\Imagick\Manager');
}
/**
* @param string $type
* @return Manager
*/
public function setType(string $type): Manager
{
$this->type = $type;
return $this;
}
/**
* @param User $user
* @return Manager
*/
public function setUser(User $user): Manager
{
$this->user = $user;
return $this;
}
/**
* @param User $actor
* @return Manager
*/
public function setActor(User $actor): Manager
{
$this->actor = $actor;
return $this;
}
/**
* @param UploadedFile $file
* @param Asset|null $asset
* @return bool
* @throws Exception
*/
public function set(UploadedFile $file, Asset $asset = null)
{
if (!$this->user) {
throw new Exception('Invalid user');
} elseif (!$this->type || !in_array($this->type, Asset::TYPES, true)) {
throw new Exception('Invalid asset type');
}
// Load image
$this->imageManager
->setImageFromBlob(
$file->getStream()->getContents(),
$file->getClientFilename()
);
// Setup asset
if (!$asset) {
$asset = new Asset();
}
$asset
->setType($this->type)
->setUserGuid($this->user->guid);
// Handle asset type
switch ($this->type) {
case 'logo':
$blob = $this->imageManager
->resize(1920, 1080, false, false) // Max: 2K
->getPng();
break;
case 'background':
$blob = $this->imageManager
->autorotate()
->resize(3840, 2160, false, false) // Max: 4K
->getJpeg(85);
break;
default:
throw new Exception('Invalid asset type handler');
}
$file = $asset->getFile();
$file->open('write');
$file->write($blob);
$file->close();
return true;
}
}
......@@ -43,10 +43,11 @@ class HydrateSettingsDelegate
public function onGet(User $user, Settings $settings): Settings
{
try {
$logoImage = $settings->getLogoGuid() ? sprintf(
'%sfs/v1/thumbnail/%s/master',
$logoImage = $settings->hasCustomLogo() ? sprintf(
'%sfs/v1/pro/%s/logo/%s',
$this->config->get('cdn_url'),
$settings->getLogoGuid()
$settings->getUserGuid(),
$settings->getTimeUpdated()
) : $user->getIconURL('large');
if ($logoImage) {
......@@ -58,17 +59,32 @@ class HydrateSettingsDelegate
}
try {
$carousels = $this->entitiesBuilder->get(['subtype' => 'carousel', 'owner_guid' => (string) $user->guid]);
$carousel = $carousels[0] ?? null;
$backgroundImage = null;
if ($carousel) {
$settings
->setBackgroundImage(sprintf(
if ($settings->hasCustomBackground()) {
$backgroundImage = sprintf(
'%sfs/v1/pro/%s/background/%s',
$this->config->get('cdn_url'),
$settings->getUserGuid(),
$settings->getTimeUpdated()
);
} else {
$carousels = $this->entitiesBuilder->get(['subtype' => 'carousel', 'owner_guid' => (string) $user->guid]);
$carousel = $carousels[0] ?? null;
if ($carousel) {
$backgroundImage = sprintf(
'%sfs/v1/banners/%s/fat/%s',
$this->config->get('cdn_url'),
$carousel->guid,
$carousel->last_updated
));
);
}
}
if ($backgroundImage) {
$settings
->setBackgroundImage($backgroundImage);
}
} catch (\Exception $e) {
error_log($e);
......
......@@ -258,18 +258,6 @@ class Manager
->setTileRatio($values['tile_ratio']);
}
if (isset($values['logo_guid']) && $values['logo_guid'] !== '') {
$image = $this->entitiesBuilder->single($values['logo_guid']);
// if the image doesn't exist or the guid doesn't correspond to an image
if (!$image || ($image->type !== 'object' || $image->subtype !== 'image')) {
throw new \Exception('logo_guid must be a valid image guid');
}
$settings
->setLogoGuid(trim($values['logo_guid']));
}
if (isset($values['footer_text'])) {
$footer_text = trim($values['footer_text']);
......@@ -322,6 +310,16 @@ class Manager
->setCustomHead($values['custom_head']);
}
if (isset($values['has_custom_logo'])) {
$settings
->setHasCustomLogo((bool) $values['has_custom_logo']);
}
if (isset($values['has_custom_background'])) {
$settings
->setHasCustomBackground((bool) $values['has_custom_background']);
}
if (isset($values['published'])) {
$this->user->setProPublished($values['published']);
$this->saveAction
......@@ -329,6 +327,8 @@ class Manager
->save();
}
$settings->setTimeUpdated(time());
$this->setupRoutingDelegate
->onUpdate($settings);
......
......@@ -39,5 +39,9 @@ class ProProvider extends Provider
$this->di->bind('Pro\Channel\Manager', function ($di) {
return new Channel\Manager();
}, ['useFactory' => true]);
$this->di->bind('Pro\Assets\Manager', function ($di) {
return new Assets\Manager();
}, ['useFactory' => true]);
}
}
......@@ -92,13 +92,15 @@ class Repository
->setTextColor($data['text_color'] ?? '')
->setPrimaryColor($data['primary_color'] ?? '')
->setPlainBackgroundColor($data['plain_background_color'] ?? '')
->setLogoGuid($data['logo_guid'] ?? '')
->setTileRatio($data['tile_ratio'] ?? '')
->setFooterText($data['footer_text'] ?? '')
->setFooterLinks($data['footer_links'] ?? [])
->setTagList($data['tag_list'] ?? [])
->setScheme($data['scheme'] ?? '')
->setCustomHead($data['custom_head'] ?? '')
->setHasCustomLogo($data['has_custom_logo'] ?? false)
->setHasCustomBackground($data['has_custom_background'] ?? false)
->setTimeUpdated($data['time_updated'] ?? 0)
;
$response[] = $settings;
......@@ -140,12 +142,14 @@ class Repository
'primary_color' => $settings->getPrimaryColor(),
'plain_background_color' => $settings->getPlainBackgroundColor(),
'tile_ratio' => $settings->getTileRatio(),
'logo_guid' => $settings->getLogoGuid(),
'footer_text' => $settings->getFooterText(),
'footer_links' => $settings->getFooterLinks(),
'tag_list' => $settings->getTagList(),
'scheme' => $settings->getScheme(),
'custom_head' => $settings->getCustomHead(),
'has_custom_logo' => $settings->hasCustomLogo(),
'has_custom_background' => $settings->hasCustomBackground(),
'time_updated' => $settings->getTimeUpdated(),
]),
];
......
......@@ -28,8 +28,6 @@ use Minds\Traits\MagicAttributes;
* @method Settings setPlainBackgroundColor(string $plainBackgroundColor)
* @method string getTileRatio()
* @method Settings setTileRatio(string $tileRatio)
* @method int|string getLogoGuid()
* @method Settings setLogoGuid(int|string $logoGuid)
* @method string getFooterText()
* @method Settings setFooterText(string $footerText)
* @method array getFooterLinks()
......@@ -48,6 +46,12 @@ use Minds\Traits\MagicAttributes;
* @method Settings setCustomHead(string $customHead)
* @method bool isPublished()
* @method Settings setPublished(bool $published)
* @method bool hasCustomLogo()
* @method Settings setHasCustomLogo(bool $customLogo)
* @method bool hasCustomBackground()
* @method Settings setHasCustomBackground(bool $customBackground)
* @method int getTimeUpdated()
* @method Settings setTimeUpdated(int $timeUpdated)
*/
class Settings implements JsonSerializable
{
......@@ -92,14 +96,17 @@ class Settings implements JsonSerializable
/** @var string */
protected $plainBackgroundColor;
/** @var int */
protected $logoGuid;
/** @var string */
protected $tileRatio = '16:9';
/** @var bool */
protected $hasCustomBackground;
/** @var string */
protected $backgroundImage;
/** @var string */
protected $tileRatio = '16:9';
/** @var bool */
protected $hasCustomLogo;
/** @var string */
protected $logoImage;
......@@ -125,6 +132,9 @@ class Settings implements JsonSerializable
/** @var bool */
protected $published;
/** @var int */
protected $timeUpdated;
/**
* @return string
*/
......@@ -155,15 +165,17 @@ class Settings implements JsonSerializable
'footer_text' => $this->footerText,
'footer_links' => $this->footerLinks,
'tag_list' => $this->tagList,
'logo_guid' => (string) $this->logoGuid,
'background_image' => $this->backgroundImage,
'has_custom_logo' => $this->hasCustomLogo,
'logo_image' => $this->logoImage,
'has_custom_background' => $this->hasCustomBackground,
'background_image' => $this->backgroundImage,
'featured_content' => $this->featuredContent,
'scheme' => $this->scheme,
'custom_head' => $this->customHead,
'one_line_headline' => $this->getOneLineHeadline(),
'styles' => $this->buildStyles(),
'published' => $this->published,
'time_updated' => $this->timeUpdated,
];
}
......
<?php
namespace Spec\Minds\Core\Pro\Assets;
use Exception;
use Minds\Core\Pro\Assets\Asset;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class AssetSpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(Asset::class);
}
public function it_should_set_type()
{
$this
->setType(Asset::TYPES[0])
->getType()
->shouldReturn(Asset::TYPES[0]);
}
public function it_should_throw_if_type_is_invalid()
{
$this
->shouldThrow(Exception::class)
->duringSetType('-!__!@_#)!@#_)!@#@!_#)INVALID');
}
}
<?php
namespace Spec\Minds\Core\Pro\Assets;
use ElggFile;
use Minds\Core\Media\Imagick\Manager as ImageManager;
use Minds\Core\Pro\Assets\Asset;
use Minds\Core\Pro\Assets\Manager;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\UploadedFile;
class ManagerSpec extends ObjectBehavior
{
/** @var ImageManager */
protected $imageManager;
public function let(
ImageManager $imageManager
) {
$this->imageManager = $imageManager;
$this->beConstructedWith($imageManager);
}
public function it_is_initializable()
{
$this->shouldHaveType(Manager::class);
}
public function it_should_set_logo(
User $user,
UploadedFile $file,
StreamInterface $fileStream,
Asset $asset,
ElggFile $assetFile
) {
$file->getStream()
->shouldBeCalled()
->willReturn($fileStream);
$fileStream->getContents()
->shouldBeCalled()
->willReturn('~image file~');
$file->getClientFilename()
->shouldBeCalled()
->willReturn('asset.img');
$this->imageManager->setImageFromBlob('~image file~', 'asset.img')
->shouldBeCalled()
->willReturn();
$asset->setType('logo')
->shouldBeCalled()
->willReturn($asset);
$user->get('guid')
->shouldBeCalled()
->willReturn(1000);
$asset->setUserGuid(1000)
->shouldBeCalled()
->willReturn($asset);
$this->imageManager->resize(Argument::cetera())
->shouldBeCalled()
->willReturn($this->imageManager);
$this->imageManager->getPng()
->shouldBeCalled()
->willReturn('~png file~');
$asset->getFile()
->shouldBeCalled()
->willReturn($assetFile);
$assetFile->open('write')
->shouldBeCalled()
->willReturn(true);
$assetFile->write('~png file~')
->shouldBeCalled()
->willReturn(true);
$assetFile->close()
->shouldBeCalled()
->willReturn(true);
$this
->setType('logo')
->setUser($user)
->setActor($user)
->set($file, $asset)
->shouldReturn(true);
}
public function it_should_set_background(
User $user,
UploadedFile $file,
StreamInterface $fileStream,
Asset $asset,
ElggFile $assetFile
) {
$file->getStream()
->shouldBeCalled()
->willReturn($fileStream);
$fileStream->getContents()
->shouldBeCalled()
->willReturn('~image file~');
$file->getClientFilename()
->shouldBeCalled()
->willReturn('asset.img');
$this->imageManager->setImageFromBlob('~image file~', 'asset.img')
->shouldBeCalled()
->willReturn();
$asset->setType('background')
->shouldBeCalled()
->willReturn($asset);
$user->get('guid')
->shouldBeCalled()
->willReturn(1000);
$asset->setUserGuid(1000)
->shouldBeCalled()
->willReturn($asset);
$this->imageManager->autorotate()
->shouldBeCalled()
->willReturn($this->imageManager);
$this->imageManager->resize(Argument::cetera())
->shouldBeCalled()
->willReturn($this->imageManager);
$this->imageManager->getJpeg(Argument::type('int'))
->shouldBeCalled()
->willReturn('~jpg file~');
$asset->getFile()
->shouldBeCalled()
->willReturn($assetFile);
$assetFile->open('write')
->shouldBeCalled()
->willReturn(true);
$assetFile->write('~jpg file~')
->shouldBeCalled()
->willReturn(true);
$assetFile->close()
->shouldBeCalled()
->willReturn(true);
$this
->setType('background')
->setUser($user)
->setActor($user)
->set($file, $asset)
->shouldReturn(true);
}
}
......@@ -43,18 +43,105 @@ class HydrateSettingsDelegateSpec extends ObjectBehavior
Activity $activity1,
Activity $activity2
) {
$settings->getLogoGuid()
$this->config->get('cdn_url')
->shouldBeCalled()
->willReturn('http://phpspec.test/');
$settings->hasCustomLogo()
->shouldBeCalled()
->willReturn(true);
$settings->getUserGuid()
->shouldBeCalled()
->willReturn(7500);
->willReturn(1000);
$settings->getTimeUpdated()
->shouldBeCalled()
->willReturn(999999);
$settings->setLogoImage('http://phpspec.test/fs/v1/pro/1000/logo/999999')
->shouldBeCalled()
->willReturn($settings);
$settings->hasCustomBackground()
->shouldBeCalled()
->willReturn(true);
$settings->setBackgroundImage('http://phpspec.test/fs/v1/pro/1000/background/999999')
->shouldBeCalled()
->willReturn($settings);
$user->getPinnedPosts()
->shouldBeCalled()
->willReturn([5000, 5001]);
$this->entitiesBuilder->get(['guids' => ['5000', '5001']])
->shouldBeCalled()
->willReturn([ $activity1, $activity2 ]);
$activity1->get('time_created')
->shouldBeCalled()
->willReturn(10000010);
$activity1->get('entity_guid')
->shouldBeCalled()
->willReturn(7400);
$activity2->get('time_created')
->shouldBeCalled()
->willReturn(10000090);
$activity2->get('guid')
->shouldBeCalled()
->willReturn(5001);
$activity2->get('entity_guid')
->shouldBeCalled()
->willReturn(null);
$settings->setFeaturedContent([5001, 7400])
->shouldBeCalled()
->willReturn($settings);
$user->isProPublished()
->willReturn(false);
$settings->setPublished(false)
->shouldBeCalled();
$this
->shouldNotThrow(Exception::class)
->duringOnGet($user, $settings);
}
public function it_should_hydrate_settings_with_default_assets_on_get(
User $user,
Settings $settings,
Carousel $carousel,
Activity $activity1,
Activity $activity2
) {
$this->config->get('cdn_url')
->shouldBeCalled()
->willReturn('http://phpspec.test/');
$settings->setLogoImage('http://phpspec.test/fs/v1/thumbnail/7500/master')
$settings->hasCustomLogo()
->shouldBeCalled()
->willReturn(false);
$user->getIconURL('large')
->shouldBeCalled()
->willReturn('http://phpspec.test/fs/v1/avatar/1000');
$settings->setLogoImage('http://phpspec.test/fs/v1/avatar/1000')
->shouldBeCalled()
->willReturn($settings);
$settings->hasCustomBackground()
->shouldBeCalled()
->willReturn(false);
$user->get('guid')
->shouldBeCalled()
->willReturn(1000);
......