Skip to content
Commits on Source (2)
......@@ -13,6 +13,7 @@ use Minds\Helpers;
use Minds\Entities;
use Minds\Interfaces;
use Minds\Api\Factory;
use Minds\Core\Features\Manager as FeaturesManager;
class thumbnails implements Interfaces\Api, Interfaces\ApiIgnorePam
{
......@@ -28,6 +29,17 @@ class thumbnails implements Interfaces\Api, Interfaces\ApiIgnorePam
exit;
}
$featuresManager = new FeaturesManager();
if ($featuresManager->has('cdn-jwt')) {
error_log("{$_SERVER['REQUEST_URI']} was hit, and should not have been");
return Factory::response([
'status' => 'error',
'message' => 'This endpoint has been deprecated. Please use fs/v1/thumbnail',
]);
}
$guid = $pages[0];
Core\Security\ACL::$ignore = true;
......
......@@ -8,6 +8,7 @@ use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Entities;
use Minds\Interfaces;
use Minds\Core\Features\Manager as FeaturesManager;
class thumbnail extends Core\page implements Interfaces\page
{
......@@ -17,6 +18,16 @@ class thumbnail extends Core\page implements Interfaces\page
exit;
}
$featuresManager = new FeaturesManager;
if ($featuresManager->has('cdn-jwt')) {
$signedUri = new Core\Security\SignedUri();
$uri = (string) \Zend\Diactoros\ServerRequestFactory::fromGlobals()->getUri();
if (!$signedUri->confirm($uri)) {
exit;
}
}
/** @var Core\Media\Thumbnails $mediaThumbnails */
$mediaThumbnails = Di::_()->get('Media\Thumbnails');
......
......@@ -9,6 +9,7 @@ use Minds\Entities\RepositoryEntity;
use Minds\Entities\User;
use Minds\Helpers\Flags;
use Minds\Helpers\Unknown;
use Minds\Core\Di\Di;
/**
* Comment Entity
......@@ -284,6 +285,21 @@ class Comment extends RepositoryEntity
return "{$this->getParentGuidL1()}:{$this->getParentGuidL2()}:{$this->getGuid()}";
}
/**
* Return an array of thumbnails
* @return array
*/
public function getThumbnails(): array
{
$thumbnails = [];
$mediaManager = Di::_()->get('Media\Image\Manager');
$sizes = [ 'xlarge', 'large' ];
foreach ($sizes as $size) {
$thumbnails[$size] = $mediaManager->getPublicAssetUri($this, $size);
}
return $thumbnails;
}
/**
* Return the urn for the comment
* @return string
......@@ -384,6 +400,8 @@ class Comment extends RepositoryEntity
$output['thumbs:down:count'] = count($this->getVotesDown());
}
$output['thumbnails'] = $this->getThumbnails();
$output['can_reply'] = (bool) !$this->getParentGuidL2();
//$output['parent_guid'] = (string) $this->entityGuid;
......
<?php
/**
* Image Manager
*/
namespace Minds\Core\Media\Image;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Entities\Entity;
use Minds\Entities\Activity;
use Minds\Entities\Image;
use Minds\Entities\Video;
use Minds\Core\Comments\Comment;
use Minds\Core\Security\SignedUri;
use Lcobucci\JWT;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Zend\Diactoros\Uri;
class Manager
{
/** @var Config $config */
private $config;
/** @var SignedUri $signedUri */
private $signedUri;
public function __construct($config = null, $signedUri = null)
{
$this->config = $config ?? Di::_()->get('Config');
$this->signedUri = $signedUri ?? new SignedUri;
}
/**
* Return a public asset uri for entity type
* @param Entity $entity
* @param string $size
* @return string
*/
public function getPublicAssetUri($entity, $size = 'xlarge'): string
{
$uri = null;
$asset_guid = null;
switch (get_class($entity)) {
case Activity::class:
switch ($entity->get('custom_type')) {
case "batch":
$asset_guid = $entity->get('entity_guid');
break;
default:
$asset_guid = $entity->get('entity_guid');
}
break;
case Image::class:
$asset_guid = $entity->getGuid();
break;
case Video::class:
$asset_guid = $entity->getGuid();
break;
case Comment::class:
$asset_guid = $entity->getAttachments()['attachment_guid'];
break;
}
$uri = $this->config->get('cdn_url') . 'fs/v1/thumbnail/' . $asset_guid . '/' . $size;
$uri = $this->signUri($uri);
return $uri;
}
/**
* Sign a uri and return the uri with the signature attached
* @param string $uri
* @return string
*/
private function signUri($uri, $pub = ""): string
{
$now = new \DateTime();
$expires = $now->modify('midnight + 30 days')->getTimestamp();
return $this->signedUri->sign($uri, $expires);
}
/**
* Config signed uri
* @param string $uri
* @return string
*/
public function confirmSignedUri($uri): bool
{
return $this->signedUri->confirm($uri);
}
}
......@@ -12,6 +12,12 @@ class MediaProvider extends Provider
{
public function register()
{
$this->di->bind('Media\Image\Manager', function ($di) {
return new Image\Manager();
}, ['useFactory' => true]);
$this->di->bind('Media\Video\Manager', function ($di) {
return new Video\Manager();
}, ['useFactory' => true]);
$this->di->bind('Media\Albums', function ($di) {
return new Albums(new Core\Data\Call('entities_by_time'));
}, ['useFactory' => true]);
......
<?php
/**
* Video Manager
*/
namespace Minds\Core\Media\Video;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Entities\Entity;
use Minds\Entities\Activity;
use Minds\Entities\Image;
use Minds\Entities\Video;
use Minds\Core\Comments\Comment;
use Aws\S3\S3Client;
class Manager
{
/** @var Config $config */
private $config;
/** @var S3Client $s3 */
private $s3;
public function __construct($config = null, $s3 = null)
{
$this->config = $config ?? Di::_()->get('Config');
// AWS
$awsConfig = $this->config->get('aws');
$opts = [
'region' => $awsConfig['region'],
];
if (!isset($awsConfig['useRoles']) || !$awsConfig['useRoles']) {
$opts['credentials'] = [
'key' => $awsConfig['key'],
'secret' => $awsConfig['secret'],
];
}
$this->s3 = $s3 ?: new S3Client(array_merge(['version' => '2006-03-01'], $opts));
}
/**
* Return a public asset uri for entity type
* @param Entity $entity
* @param string $size
* @return string
*/
public function getPublicAssetUri($entity, $size = '360.mp4'): string
{
$cmd = null;
switch (get_class($entity)) {
case Activity::class:
// To do
break;
case Video::class:
$cmd = $this->s3->getCommand('GetObject', [
'Bucket' => 'cinemr', // TODO: don't hard code
'Key' => $this->config->get('transcoder')['dir'] . "/" . $entity->get('cinemr_guid') . "/" . $size,
]);
break;
}
if (!$cmd) {
return null;
}
return (string) $this->s3->createPresignedRequest($cmd, '+20 minutes')->getUri();
}
}
<?php
namespace Minds\Core\Security;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Lcobucci\JWT;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Zend\Diactoros\Uri;
class SignedUri
{
/** @Var JWT\Builder $jwtBuilder */
private $jwtBuilder;
/** @var JWT\Parser $jwtParser */
private $jwtParser;
/** @var Config $config */
private $config;
public function __construct($jwtBuilder = null, $jwtParser = null, $config = null)
{
$this->jwtBuilder = $jwtBuilder ?? new JWT\Builder;
$this->jwtParser = $jwtParser ?? new JWT\Parser();
$this->config = $config ?? Di::_()->get('Config');
}
/**
* Sign the uri
* @param string $uri
* @param int $ttl - defaults to 1 day
* @return string
*/
public function sign($uri, $expires = 86400): string
{
$uri = new Uri($uri);
$expires = (new \DateTime())->modify('midnight first day of next month')->modify('+1 month')->getTimestamp();
$token = (new $this->jwtBuilder)
//->setId((string) $uri)
->setExpiration($expires)
->set('uri', (string) $uri)
->set('user_guid', (string) Session::getLoggedInUser()->getGuid())
->sign(new Sha256, $this->config->get('sessions')['private_key'])
->getToken();
$signedUri = $uri->withQuery("jwtsig=$token");
return (string) $signedUri;
}
/**
* Confirm signed uri
* @param string $uri
* @return string
*/
public function confirm($uri): bool
{
$providedUri = new Uri($uri);
parse_str($providedUri->getQuery(), $queryParams);
$providedSig = $queryParams['jwtsig'];
$token = $this->jwtParser->parse($providedSig);
if (!$token->verify(new Sha256, $this->config->get('sessions')['private_key'])) {
return false;
}
return ((string) $token->getClaim('uri') === (string) $providedUri->withQuery(''));
}
}
......@@ -67,7 +67,7 @@ class S3 implements ServiceInterface
$mimeType = $finfo->buffer($data);
$write = $this->s3->putObject([
'ACL' => 'public-read',
// 'ACL' => 'public-read',
'Bucket' => Config::_()->aws['bucket'],
'Key' => $this->filepath,
'ContentType' => $mimeType,
......@@ -121,6 +121,20 @@ class S3 implements ServiceInterface
}
}
/**
* Return a signed url
* @return string
*/
public function getSignedUrl(): string
{
$cmd = $this->s3->getCommand('GetObject', [
'Bucket' => Config::_()->aws['bucket'],
'Key' => $this->filepath,
]);
$request = $this->s3->createPresignedRequest($cmd, '+20 minutes');
return (string) $request->getUri();
}
public function seek($offset = 0)
{
//not supported
......
......@@ -3,6 +3,7 @@ namespace Minds\Entities;
use Minds\Helpers;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Core\Queue;
use Minds\Core\Analytics;
......@@ -279,6 +280,8 @@ class Activity extends Entity
$export['hide_impressions'] = $this->hide_impressions;
}
$export['thumbnails'] = $this->getThumbnails();
switch ($this->custom_type) {
case 'video':
if ($this->custom_data['guid']) {
......@@ -289,7 +292,11 @@ class Activity extends Entity
// fix old images src
if (is_array($export['custom_data']) && strpos($export['custom_data'][0]['src'], '/wall/attachment') !== false) {
$export['custom_data'][0]['src'] = Core\Config::_()->cdn_url . 'fs/v1/thumbnail/' . $this->entity_guid;
$this->custom_data[0]['src'] = $export['custom_data'][0]['src'];
}
// go directly to cdn
$mediaManager = Di::_()->get('Media\Image\Manager');
$export['custom_data'][0]['src'] = $export['thumbnails']['xlarge'];
break;
}
......@@ -720,6 +727,27 @@ class Activity extends Entity
return $this->ownerObj;
}
/**
* Return thumbnails array to be used with export
* @return array
*/
public function getThumbnails(): array
{
$thumbnails = [];
switch ($this->custom_type) {
case 'video':
break;
case 'batch':
$mediaManager = Di::_()->get('Media\Image\Manager');
$sizes = [ 'xlarge', 'large' ];
foreach ($sizes as $size) {
$thumbnails[$size] = $mediaManager->getPublicAssetUri($this, $size);
}
break;
}
return $thumbnails;
}
/**
* Return a preferred urn
* @return string
......
......@@ -5,6 +5,7 @@
namespace Minds\Entities;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Helpers;
class Image extends File
......@@ -33,17 +34,18 @@ class Image extends File
$size = '';
}
if (isset($CONFIG->cdn_url) && !$this->getFlag('paywall') && !$this->getWireThreshold()) {
$base_url = $CONFIG->cdn_url;
} else {
$base_url = \elgg_get_site_url();
}
// if (isset($CONFIG->cdn_url) && !$this->getFlag('paywall') && !$this->getWireThreshold()) {
// $base_url = $CONFIG->cdn_url;
// } else {
// $base_url = \elgg_get_site_url();
// }
if ($this->access_id != 2) {
$base_url = \elgg_get_site_url();
}
// if ($this->access_id != 2) {
// $base_url = \elgg_get_site_url();
// }
$mediaManager = Di::_()->get('Media\Image\Manager');
return $base_url. 'api/v1/media/thumbnails/' . $this->guid . '/'.$size;
return $mediaManager->getPublicAssetUri($this, $size);
}
protected function getIndexKeys($ia = false)
......@@ -211,7 +213,8 @@ class Image extends File
public function export()
{
$export = parent::export();
$export['thumbnail_src'] = $this->getIconUrl();
$export['thumbnail_src'] = $this->getIconUrl('xlarge');
$export['thumbnail'] = $export['thumbnail_src'];
$export['thumbs:up:count'] = Helpers\Counters::get($this->guid, 'thumbs:up');
$export['thumbs:down:count'] = Helpers\Counters::get($this->guid, 'thumbs:down');
$export['description'] = $this->description; //videos need to be able to export html.. sanitize soon!
......
......@@ -8,6 +8,7 @@ namespace Minds\Entities;
use Minds\Core;
use Minds\Core\Media\Services\Factory as ServiceFactory;
use Minds\Core\Di\Di;
use cinemr;
use Minds\Helpers;
......@@ -38,8 +39,8 @@ class Video extends Object
*/
public function getSourceUrl($transcode = '720.mp4')
{
$url = Core\Config::_()->cinemr_url . $this->cinemr_guid . '/' . $transcode;
return $url;
$mediaManager = Di::_()->get('Media\Video\Manager');
return $mediaManager->getPublicAssetUri($this, $transcode);
}
/**
......@@ -61,13 +62,16 @@ class Video extends Object
public function getIconUrl($size = "medium")
{
$domain = elgg_get_site_url();
global $CONFIG;
if (isset($CONFIG->cdn_url) && !$this->getFlag('paywall') && !$this->getWireThreshold()) {
$domain = $CONFIG->cdn_url;
}
// $domain = elgg_get_site_url();
// global $CONFIG;
// if (isset($CONFIG->cdn_url) && !$this->getFlag('paywall') && !$this->getWireThreshold()) {
// $domain = $CONFIG->cdn_url;
// }
// return $domain . 'api/v1/media/thumbnails/' . $this->guid . '/' . $this->time_updated;
return $domain . 'api/v1/media/thumbnails/' . $this->guid . '/' . $this->time_updated;
$mediaManager = Di::_()->get('Media\Image\Manager');
return $mediaManager->getPublicAssetUri($this, 'medium');
}
public function getURL()
......
<?php
namespace Spec\Minds\Core\Media\Image;
use Minds\Core\Media\Image\Manager;
use Minds\Core\Config;
use Minds\Core\Security\SignedUri;
use Minds\Entities\Activity;
use Minds\Entities\Image;
use Minds\Entities\Video;
use Minds\Core\Comments\Comment;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ManagerSpec extends ObjectBehavior
{
private $config;
private $signedUri;
public function let(Config $config, SignedUri $signedUri)
{
$this->beConstructedWith($config, $signedUri);
$this->config = $config;
$this->signedUri = $signedUri;
}
public function it_is_initializable()
{
$this->shouldHaveType(Manager::class);
}
public function it_should_return_public_asset_uri()
{
$activity = new Activity();
$activity->set('entity_guid', 123);
$this->config->get('cdn_url')
->willReturn('https://minds.dev/');
$uri = 'https://minds.dev/fs/v1/thumbnail/123/xlarge';
$this->signedUri->sign($uri, Argument::any())
->willReturn('signed url will be here');
$this->getPublicAssetUri($activity)
->shouldBe('signed url will be here');
}
public function it_should_return_public_asset_uri_for_image()
{
$entity = new Image();
$entity->set('guid', 123);
$this->config->get('cdn_url')
->willReturn('https://minds.dev/');
$uri = 'https://minds.dev/fs/v1/thumbnail/123/xlarge';
$this->signedUri->sign($uri, Argument::any())
->willReturn('signed url will be here');
$this->getPublicAssetUri($entity)
->shouldBe('signed url will be here');
}
public function it_should_return_public_asset_uri_for_video()
{
$entity = new Video();
$entity->set('guid', 123);
$this->config->get('cdn_url')
->willReturn('https://minds.dev/');
$uri = 'https://minds.dev/fs/v1/thumbnail/123/xlarge';
$this->signedUri->sign($uri, Argument::any())
->willReturn('signed url will be here');
$this->getPublicAssetUri($entity)
->shouldBe('signed url will be here');
}
public function it_should_return_public_asset_uri_for_comment()
{
$entity = new Comment();
$entity->setAttachment('attachment_guid', '123');
$this->config->get('cdn_url')
->willReturn('https://minds.dev/');
$uri = 'https://minds.dev/fs/v1/thumbnail/123/xlarge';
$this->signedUri->sign($uri, Argument::any())
->willReturn('signed url will be here');
$this->getPublicAssetUri($entity)
->shouldBe('signed url will be here');
}
}
<?php
namespace Spec\Minds\Core\Media\Video;
use Minds\Core\Config;
use Aws\S3\S3Client;
use Minds\Core\Media\Video\Manager;
use Minds\Entities\Video;
use Psr\Http\Message\RequestInterface;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ManagerSpec extends ObjectBehavior
{
private $config;
private $s3;
public function let(Config $config, S3Client $s3)
{
$this->beConstructedWith($config, $s3);
$this->config = $config;
$this->s3 = $s3;
}
public function it_is_initializable()
{
$this->shouldHaveType(Manager::class);
}
public function it_should_get_a_720p_video(RequestInterface $request, \Aws\CommandInterface $cmd)
{
$this->config->get('transcoder')
->willReturn([
'dir' => 'dir',
]);
$this->config->get('aws')
->willReturn([
'region' => 'us-east-1',
'useRoles' => true,
]);
$this->s3->getCommand('GetObject', [
'Bucket' => 'cinemr',
'Key' => 'dir/123/720.mp4'
])
->shouldBeCalled()
->willReturn($cmd);
$request->getUri()
->willReturn('s3-signed-url-here');
$this->s3->createPresignedRequest(Argument::any(), Argument::any())
->willReturn($request);
$video = new Video();
$video->set('cinemr_guid', 123);
$this->getPublicAssetUri($video, '720.mp4')
->shouldBe('s3-signed-url-here');
}
}
<?php
namespace Spec\Minds\Core\Security;
use Minds\Core\Security\SignedUri;
use Minds\Entities\User;
use Minds\Core\Config;
use Lcobucci\JWT;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class SignedUriSpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(SignedUri::class);
}
public function it_sign_a_uri(Config $config)
{
$user = new User();
$user->set('guid', 123);
$user->set('username', 'phpspec');
\Minds\Core\Session::setUser($user);
$this->beConstructedWith(null, null, $config);
$config->get('sessions')
->willReturn([
'private_key' => 'priv-key'
]);
$this->sign("https://minds-dev/foo")
->shouldContain("https://minds-dev/foo?jwtsig=");
}
public function it_should_verify_a_uri_was_signed(Config $config)
{
$this->beConstructedWith(null, null, $config);
$config->get('sessions')
->willReturn([
'private_key' => 'priv-key'
]);
$uri = "https://minds-dev/foo?jwtsig=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NzI1NjY0MDAsInVyaSI6Imh0dHBzOlwvXC9taW5kcy1kZXZcL2ZvbyIsInVzZXJfZ3VpZCI6IjEyMyJ9.jqOq0k-E4h1I0PHnc_WkmWqXonRU4yWq_ymoOYoaDvc";
$this->confirm($uri)
->shouldBe(true);
}
public function it_should_not_very_a_wrongly_signed_uri(Config $config)
{
$this->beConstructedWith(null, null, $config);
$config->get('sessions')
->willReturn([
'private_key' => 'priv-key'
]);
$uri = "https://minds-dev/bar?jwtsig=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NzI1NjY0MDAsInVyaSI6Imh0dHBzOlwvXC9taW5kcy1kZXZcL2ZvbyIsInVzZXJfZ3VpZCI6IjEyMyJ9.jqOq0k-E4h1I0PHnc_WkmWqXonRU4yWq_ymoOYoaDvc";
$this->confirm($uri)
->shouldBe(false);
}
}