...
 
Commits (4)
......@@ -2,6 +2,8 @@
namespace Minds\Api;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Domain\Security as ProDomainSecurity;
use Minds\Interfaces;
use Minds\Helpers;
use Minds\Core\Security;
......@@ -32,6 +34,7 @@ class Factory
$loop = count($segments);
while ($loop >= 0) {
$offset = $loop -1;
if ($loop < count($segments)) {
$slug_length = strlen($segments[$offset+1].'\\');
$route_length = strlen($route);
......@@ -42,32 +45,51 @@ class Factory
$actual = str_replace('\\', '/', $route);
if (isset(Routes::$routes[$actual])) {
$class_name = Routes::$routes[$actual];
if (class_exists($class_name)) {
$handler = new $class_name();
if (property_exists($handler, 'request')) {
$handler->request = $request;
}
if ($handler instanceof Interfaces\ApiAdminPam) {
self::adminCheck();
}
if (!$handler instanceof Interfaces\ApiIgnorePam) {
self::pamCheck($request, $response);
}
$pages = array_splice($segments, $loop) ?: [];
return $handler->$method($pages);
}
}
//autloaded routes
$class_name = "\\Minds\\Controllers\api\\$route";
if (class_exists($class_name)) {
$handler = new $class_name();
if (property_exists($handler, 'request')) {
$handler->request = $request;
}
if ($handler instanceof Interfaces\ApiAdminPam) {
self::adminCheck();
}
if (!$handler instanceof Interfaces\ApiIgnorePam) {
self::pamCheck($request, $response);
}
$pages = array_splice($segments, $loop) ?: [];
return $handler->$method($pages);
}
--$loop;
}
}
......@@ -78,15 +100,18 @@ class Factory
*/
public static function pamCheck($request, $response)
{
if ($request->getAttribute('oauth_user_id')
|| Security\XSRF::validateRequest()
if (
$request->getAttribute('oauth_user_id') ||
Security\XSRF::validateRequest()
) {
return true;
} else {
//error_log('failed authentication:: OAUTH via API');
ob_end_clean();
static::setCORSHeader();
header('Content-type: application/json');
header("Access-Control-Allow-Origin: *");
header('HTTP/1.1 401 Unauthorized', true, 401);
echo json_encode([
'error' => 'Sorry, you are not authenticated',
......@@ -108,8 +133,10 @@ class Factory
} else {
error_log('security: unauthorized access to admin api');
ob_end_clean();
static::setCORSHeader();
header('Content-type: application/json');
header("Access-Control-Allow-Origin: *");
header('HTTP/1.1 401 Unauthorized', true, 401);
echo json_encode(['error'=>'You are not an admin', 'code'=>401]);
exit;
......@@ -126,8 +153,10 @@ class Factory
return true;
} else {
ob_end_clean();
static::setCORSHeader();
header('Content-type: application/json');
header("Access-Control-Allow-Origin: *");
header('HTTP/1.1 401 Unauthorized', true, 401);
echo json_encode([
'status' => 'error',
......@@ -151,11 +180,26 @@ class Factory
ob_end_clean();
static::setCORSHeader();
header('Content-type: application/json');
header("Access-Control-Allow-Origin: *");
echo json_encode($data);
}
/**
* Sets the CORS header, if not already set
*/
public static function setCORSHeader(): void
{
$wasSet = count(array_filter(headers_list(), function ($header) {
return stripos($header, 'Access-Control-Allow-Origin:') === 0;
})) > 0;
if (!$wasSet) {
header("Access-Control-Allow-Origin: *");
}
}
/**
* Returns the exportable form of the entities
* @param array $entities - an array of entities
......
<?php
/**
* Jwt
* @author edgebal
*/
namespace Minds\Common;
use Exception;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Claim;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer\Hmac\Sha256;
class Jwt
{
/** @var string */
protected $key;
/**
* @param string $key
* @return Jwt
*/
public function setKey(string $key): Jwt
{
$this->key = $key;
return $this;
}
/**
* @param object|array $payload
* @param int|null $exp
* @param int|null $nbf
* @return string
* @throws Exception
*/
public function encode($payload, $exp = null, $nbf = null): string
{
if (!$this->key) {
throw new Exception('Invalid JWT key');
}
$builder = new Builder();
foreach ($payload as $key => $value) {
$builder->set($key, $value);
}
if ($exp !== null) {
$builder->setExpiration($exp);
}
if ($nbf !== null) {
$builder->setNotBefore($nbf);
}
$builder->sign(new Sha256(), $this->key);
return (string) $builder->getToken();
}
/**
* @param string $jwt
* @return array
* @throws Exception
*/
public function decode($jwt): array
{
if (!$this->key) {
throw new Exception('Invalid JWT key');
}
$token = (new Parser())->parse($jwt);
if (!$token->verify(new Sha256(), $this->key)) {
throw new Exception('Invalid JWT');
}
return array_map(function (Claim $claim) {
return $claim->getValue();
}, $token->getClaims());
}
/**
* @return string
*/
public function randomString(): string
{
$bytes = openssl_random_pseudo_bytes(128);
return hash('sha512', $bytes);
}
}
......@@ -328,4 +328,13 @@ class Response implements \Iterator, \ArrayAccess, \Countable, \JsonSerializable
{
return array_reduce($this->data, $callback, $initialValue);
}
/**
* Returns the first element of the Response, or null if empty
* @return mixed|null
*/
public function first()
{
return $this->data[0] ?? null;
}
}
......@@ -16,6 +16,8 @@ use Minds\Entities;
use Minds\Interfaces;
use Minds\Api\Factory;
use Minds\Exceptions\TwoFactorRequired;
use Minds\Core\Queue;
use Minds\Core\Subscriptions;
class authenticate implements Interfaces\Api, Interfaces\ApiIgnorePam
{
......@@ -44,6 +46,7 @@ class authenticate implements Interfaces\Api, Interfaces\ApiIgnorePam
}
$user = new Entities\User(strtolower($_POST['username']));
/** @var Core\Security\LoginAttempts $attempts */
$attempts = Core\Di\Di::_()->get('Security\LoginAttempts');
......@@ -117,7 +120,7 @@ class authenticate implements Interfaces\Api, Interfaces\ApiIgnorePam
public function delete($pages)
{
$sessions = Di::_()->get('Sessions\Manager');
if (isset($pages[0]) && $pages[0] === 'all') {
$sessions->destroy(true);
} else {
......
......@@ -13,6 +13,7 @@ use Minds\Interfaces;
use Minds\Entities;
use Minds\Api\Factory;
use Minds\Common\ChannelMode;
use Minds\Core\Di\Di;
use ElggFile;
class channel implements Interfaces\Api
......@@ -46,6 +47,10 @@ class channel implements Interfaces\Api
return Factory::response(['status'=>'error', 'message'=>'The user is banned']);
}
Di::_()->get('Referrals\Cookie')
->setEntity($user)
->create();
$user->fullExport = true; //get counts
$user->exportCounts = true;
$return = Factory::exportable([$user]);
......@@ -87,6 +92,19 @@ class channel implements Interfaces\Api
$block = Core\Security\ACL\Block::_();
$response['channel']['blocked'] = $block->isBlocked($user);
if ($user->isPro()) {
/** @var Core\Pro\Manager $manager */
$manager = Core\Di\Di::_()->get('Pro\Manager');
$manager
->setUser($user);
$proSettings = $manager->get();
if ($proSettings) {
$response['channel']['pro_settings'] = $proSettings;
}
}
return Factory::response($response);
}
......
......@@ -5,14 +5,14 @@
* @version 1
* @author Mark Harding
*/
namespace Minds\Controllers\api\v1;
use Minds\Api\Factory;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Entities;
use Minds\Entities\User;
use Minds\Interfaces;
use Minds\Api\Factory;
use Minds\Helpers;
class register implements Interfaces\Api, Interfaces\ApiIgnorePam
{
......@@ -21,7 +21,7 @@ class register implements Interfaces\Api, Interfaces\ApiIgnorePam
*/
public function get($pages)
{
return Factory::response(['status'=>'error', 'message'=>'GET is not supported for this endpoint']);
return Factory::response(['status' => 'error', 'message' => 'GET is not supported for this endpoint']);
}
/**
......@@ -37,11 +37,11 @@ class register implements Interfaces\Api, Interfaces\ApiIgnorePam
public function post($pages)
{
if (!isset($_POST['username']) || !isset($_POST['password']) || !isset($_POST['username']) || !isset($_POST['email'])) {
return Factory::response(['status'=>'error']);
return Factory::response(['status' => 'error']);
}
if (!$_POST['username'] || !$_POST['password'] || !$_POST['username'] || !$_POST['email']) {
return Factory::response(['status'=>'error', 'message' => "Please fill out all the fields"]);
return Factory::response(['status' => 'error', 'message' => "Please fill out all the fields"]);
}
try {
......@@ -119,11 +119,11 @@ class register implements Interfaces\Api, Interfaces\ApiIgnorePam
$sessions->save(); // Save to db and cookie
$response = [
'guid' => $guid,
'user' => $user->export()
'guid' => $guid,
'user' => $user->export(),
];
} catch (\Exception $e) {
$response = ['status'=>'error', 'message'=>$e->getMessage()];
$response = ['status' => 'error', 'message' => $e->getMessage()];
}
return Factory::response($response);
}
......
......@@ -69,10 +69,6 @@ class settings implements Interfaces\Api
{
Factory::isLoggedIn();
if (!Core\Security\XSRF::validateRequest()) {
//return false;
}
if (Core\Session::getLoggedInUser()->isAdmin() && isset($pages[0])) {
$user = new entities\User($pages[0]);
} else {
......
<?php
/**
* pro
*
* @author edgebal
*/
namespace Minds\Controllers\api\v2\admin;
use Minds\Api\Factory;
use Minds\Core\Pro\Manager;
use Minds\Entities\User as UserEntity;
use Minds\Interfaces;
use Minds\Core\Di\Di;
class pro implements Interfaces\Api, Interfaces\ApiAdminPam
{
/**
* 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
*/
public function post($pages)
{
return Factory::response([]);
}
/**
* Equivalent to HTTP PUT method
* @param array $pages
* @return mixed|null
*/
public function put($pages)
{
if (!($pages[0] ?? null)) {
return Factory::response([
'status' => 'error',
'message' => 'Emtpy target',
]);
}
$target = new UserEntity($pages[0]);
if (!$target || !$target->guid) {
return Factory::response([
'status' => 'error',
'message' => 'Invalid target',
]);
}
/** @var Manager $manager */
$manager = Di::_()->get('Pro\Manager');
$manager
->setUser($target);
$success = $manager->enable(time() + (365 * 86400));
if (!$success) {
return Factory::response([
'status' => 'error',
'message' => 'Error disabling Pro',
]);
}
return Factory::response([]);
}
/**
* Equivalent to HTTP DELETE method
* @param array $pages
* @return mixed|null
*/
public function delete($pages)
{
if (!($pages[0] ?? null)) {
return Factory::response([
'status' => 'error',
'message' => 'Emtpy target',
]);
}
$target = new UserEntity($pages[0]);
if (!$target || !$target->guid) {
return Factory::response([
'status' => 'error',
'message' => 'Invalid target',
]);
}
/** @var Manager $manager */
$manager = Di::_()->get('Pro\Manager');
$manager
->setUser($target);
$success = $manager->disable();
if (!$success) {
return Factory::response([
'status' => 'error',
'message' => 'Error disabling Pro',
]);
}
return Factory::response([]);
}
}
......@@ -113,6 +113,10 @@ class views implements Interfaces\Api
error_log($e);
}
Di::_()->get('Referrals\Cookie')
->setEntity($activity)
->create();
break;
}
......
<?php
/**
* pro
* @author edgebal
*/
namespace Minds\Controllers\api\v2;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Manager;
use Minds\Core\Session;
use Minds\Interfaces;
use Minds\Api\Factory;
class pro implements Interfaces\Api
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
* @throws Exception
*/
public function get($pages)
{
/** @var Manager $manager */
$manager = Di::_()->get('Pro\Manager');
$manager
->setUser(Session::getLoggedinUser());
return Factory::response([
'isActive' => $manager->isActive(),
]);
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed
*/
public function post($pages)
{
return Factory::response([
'status' => 'error',
'message' => 'Minds Pro is not yet publicly available.',
]);
// /** @var Manager $manager */
// $manager = Di::_()->get('Pro\Manager');
// $manager
// ->setUser(Session::getLoggedinUser());
//
// // TODO: Send and process payment data
// $success = $manager->enable(time() + (365 * 86400));
//
// if (!$success) {
// return Factory::response([
// 'status' => 'error',
// 'message' => 'Error activating Pro',
// ]);
// }
//
// return Factory::response([
// 'isActive' => $manager->isActive(),
// 'settings' => $manager->get(),
// ]);
}
/**
* 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
*/
public function delete($pages)
{
/** @var Manager $manager */
$manager = Di::_()->get('Pro\Manager');
$manager
->setUser(Session::getLoggedinUser());
$success = $manager->disable();
if (!$success) {
return Factory::response([
'status' => 'error',
'message' => 'Error disabling Pro',
]);
}
return Factory::response([]);
}
}
<?php
/**
* channel
* @author edgebal
*/
namespace Minds\Controllers\api\v2\pro;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Channel\Manager as ChannelManager;
use Minds\Core\Pro\Manager;
use Minds\Core\Session;
use Minds\Entities\User;
use Minds\Helpers\Campaigns\Referrals;
use Minds\Interfaces;
use Minds\Api\Factory;
class channel implements Interfaces\Api
{
public $request;
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
* @throws Exception
*/
public function get($pages)
{
$currentUser = Session::getLoggedinUser();
$channel = new User($pages[0]);
$channel->fullExport = true; //get counts
$channel->exportCounts = true;
/** @var Manager $manager */
$manager = Di::_()->get('Pro\Manager');
$manager->setUser($channel);
/** @var ChannelManager $manager */
$channelManager = Di::_()->get('Pro\Channel\Manager');
$channelManager->setUser($channel);
switch ($pages[1] ?? '') {
case 'content':
return Factory::response([
'content' => $channelManager->getAllCategoriesContent(),
]);
default:
$proSettings = $manager->get();
$exportedChannel = $channel->export();
$exportedChannel['pro_settings'] = $proSettings;
$origin = strtolower($this->request->getServerParams()['HTTP_X_MINDS_ORIGIN'] ?? '');
$domain = strtolower($proSettings->getDomain());
if ($domain === $origin) {
Referrals::register($channel->username);
}
return Factory::response([
'channel' => $exportedChannel,
'me' => $currentUser ? $currentUser->export() : null,
]);
}
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
*/
public function post($pages)
{
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
/**
* @author: eiennohi.
*/
namespace Minds\Controllers\api\v2\pro;
use Minds\Api\Exportable;
use Minds\Api\Factory;
use Minds\Common\Repository\Response;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Entities\Factory as EntitiesFactory;
use Minds\Entities\User;
use Minds\Interfaces;
class content implements Interfaces\Api
{
const MIN_COUNT = 6;
public function get($pages)
{
/** @var User $currentUser */
$currentUser = Core\Session::getLoggedinUser();
$container_guid = $pages[0] ?? null;
$owner_guid = null;
if (!$container_guid) {
return Factory::response([
'status' => 'error',
'message' => 'Invalid container',
]);
}
$container = EntitiesFactory::build($container_guid);
if (!$container || !Core\Security\ACL::_()->read($container, $currentUser)) {
return Factory::response([
'status' => 'error',
'message' => 'Forbidden',
]);
}
$type = '';
switch ($pages[1]) {
case 'activities':
$type = 'activity';
break;
case 'images':
$type = 'object:image';
break;
case 'videos':
$type = 'object:video';
break;
case 'blogs':
$type = 'object:blog';
break;
case 'groups':
$type = 'group';
$container_guid = null;
$owner_guid = $pages[0];
break;
case 'all':
$type = 'all';
break;
}
$hardLimit = 5000;
$offset = 0;
if (isset($_GET['offset'])) {
$offset = intval($_GET['offset']);
}
$limit = 12;
if (isset($_GET['limit'])) {
$limit = abs(intval($_GET['limit']));
}
if (($offset + $limit) > $hardLimit) {
$limit = $hardLimit - $offset;
}
if ($limit <= 0) {
return Factory::response([
'status' => 'success',
'entities' => [],
'load-next' => $hardLimit,
'overflow' => true,
]);
}
$sync = (bool) ($_GET['sync'] ?? false);
$fromTimestamp = $_GET['from_timestamp'] ?? 0;
$exclude = explode(',', $_GET['exclude'] ?? '');
$forcePublic = (bool) ($_GET['force_public'] ?? false);
$asActivities = (bool) ($_GET['as_activities'] ?? true);
$query = null;
if (isset($_GET['query'])) {
$query = $_GET['query'];
}
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
$entities->setActor($currentUser);
$isOwner = false;
if ($currentUser) {
$entities->setActor($currentUser);
$isOwner = $currentUser->guid == $container_guid;
}
$opts = [
'cache_key' => $currentUser ? $currentUser->guid : null,
'container_guid' => $container_guid,
'owner_guid' => $owner_guid,
'access_id' => $isOwner && !$forcePublic ? [0, 1, 2, $container_guid] : [2, $container_guid],
'custom_type' => null,
'limit' => $limit,
'type' => $type,
'algorithm' => 'top',
'period' => '7d',
'sync' => $sync,
'from_timestamp' => $fromTimestamp,
'query' => $query,
'single_owner_threshold' => 0,
'pinned_guids' => $type === 'activity' ? array_reverse($container->getPinnedPosts()) : null,
'exclude' => $exclude,
];
if (isset($_GET['nsfw'])) {
$nsfw = $_GET['nsfw'] ?? '';
$opts['nsfw'] = explode(',', $nsfw);
}
$hashtag = null;
if (isset($_GET['hashtag'])) {
$hashtag = strtolower($_GET['hashtag']);
}
if ($hashtag) {
$opts['hashtags'] = [$hashtag];
$opts['filter_hashtags'] = true;
} elseif (isset($_GET['hashtags']) && $_GET['hashtags']) {
$opts['hashtags'] = explode(',', $_GET['hashtags']);
$opts['filter_hashtags'] = true;
}
try {
$result = $this->getData($entities, $opts, $asActivities, $sync);
if ($result->count() <= static::MIN_COUNT) {
$opts['algorithm'] = 'latest';
$result = $this->getData($entities, $opts, $asActivities, $sync);
}
$response = [
'status' => 'success',
'entities' => Exportable::_($result),
'load-next' => $result->getPagingToken(),
];
return Factory::response($response);
} catch (\Exception $e) {
error_log($e);
return Factory::response(['status' => 'error', 'message' => $e->getMessage()]);
}
}
/**
* @param Core\Feeds\Top\Entities $entities
* @param array $opts
* @param bool $asActivities
* @param bool $sync
* @return Response
* @throws \Exception
*/
private function getData($entities, $opts, $asActivities, $sync)
{
/** @var Core\Feeds\Top\Manager $manager */
$manager = Di::_()->get('Feeds\Top\Manager');
$result = $manager->getList($opts);
if (!$sync) {
// Remove all unlisted content, if ES document is not in sync, it'll
// also remove pending activities
$result = $result->filter([$entities, 'filter']);
if ($asActivities) {
// Cast to ephemeral Activity entities, if another type
$result = $result->map([$entities, 'cast']);
}
}
return $result;
}
public function post($pages)
{
return Factory::response([]);
}
public function put($pages)
{
return Factory::response([]);
}
public function delete($pages)
{
return Factory::response([]);
}
}
<?php
/**
* settings
* @author edgebal
*/
namespace Minds\Controllers\api\v2\pro;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Manager;
use Minds\Core\Session;
use Minds\Entities\User;
use Minds\Interfaces;
use Minds\Api\Factory;
class settings implements Interfaces\Api
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
*/
public function get($pages)
{
$user = Session::getLoggedinUser();
if (isset($pages[0]) && $pages[0]) {
if (!Session::isAdmin()) {
return Factory::response([
'status' => 'error',
'message' => 'You are not authorized',
]);
}
$user = new User($pages[0]);
}
/** @var Manager $manager */
$manager = Di::_()->get('Pro\Manager');
$manager
->setUser($user)
->setActor(Session::getLoggedinUser());
return Factory::response([
'isActive' => $manager->isActive(),
'settings' => $manager->get(),
]);
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
*/
public function post($pages)
{
$user = Session::getLoggedinUser();
if (isset($pages[0]) && $pages[0]) {
if (!Session::isAdmin()) {
return Factory::response([
'status' => 'error',
'message' => 'You are not authorized',
]);
}
$user = new User($pages[0]);
}
/** @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',
]);
}
try {
$success = $manager->set($_POST);
if (!$success) {
throw new Exception('Cannot save Pro settings');
}
} 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([]);
}
}
......@@ -46,7 +46,15 @@ class thumbnail extends Core\page implements Interfaces\page
$contents = $thumbnail->read();
}
header('Content-type: image/jpeg');
try {
$finfo = new \finfo(FILEINFO_MIME);
$contentType = $finfo->buffer($contents) ?: 'image/jpeg';
} catch (\Exception $e) {
error_log($e);
$contentType = 'image/jpeg';
}
header('Content-type: ' . $contentType);
header('Expires: ' . date('r', strtotime('today + 6 months')), true);
header('Pragma: public');
header('Cache-Control: public');
......
......@@ -6,6 +6,7 @@ use Minds\Core;
use Minds\Entities;
use Minds\Helpers;
use Minds\Helpers\Counters;
use Zend\Diactoros\ServerRequestFactory;
class SEO
{
......@@ -76,18 +77,30 @@ class SEO
$params = $event->getParameters();
$slugs = $params['slugs'];
if ((count($slugs) < 3) || ($slugs[1] != 'blog')) {
/** @var Core\Pro\Domain $proDomain */
$proDomain = Core\Di\Di::_()->get('Pro\Domain');
$request = ServerRequestFactory::fromGlobals();
$serverParams = $request->getServerParams() ?? [];
$host = parse_url($serverParams['HTTP_ORIGIN'] ?? '', PHP_URL_HOST) ?: $serverParams['HTTP_HOST'];
$proSettings = $proDomain->lookup($host);
if ($proSettings && (count($slugs) < 2 || $slugs[0] === 'blog')) {
$slugParts = explode('-', $slugs[1]);
} elseif (!$proSettings && count($slugs) >= 3 && $slugs[1] === 'blog') {
$slugParts = explode('-', $slugs[2]);
} else {
return;
}
$slugParts = explode('-', $slugs[2]);
$guid = $slugParts[count($slugParts) - 1];
if (!is_numeric($guid)) {
return;
}
$event->setResponse($this->viewHandler([ $guid ]));
$event->setResponse($this->viewHandler([$guid]));
});
}
......
......@@ -7,6 +7,7 @@
namespace Minds\Core\Feeds;
use JsonSerializable;
use Minds\Traits\Exportable;
use Minds\Traits\MagicAttributes;
......@@ -22,7 +23,7 @@ use Minds\Traits\MagicAttributes;
* @method string getUrn()
* @method FeedSyncEntity setUrn(string $urn)
*/
class FeedSyncEntity
class FeedSyncEntity implements JsonSerializable
{
use MagicAttributes;
......@@ -55,4 +56,16 @@ class FeedSyncEntity
'entity' => $this->entity ? $this->entity->export() : null,
];
}
/**
* Specify data which should be serialized to JSON
* @link https://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize(): array
{
return $this->export();
}
}
......@@ -73,6 +73,7 @@ class Minds extends base
(new \Minds\Entities\EntitiesProvider())->register();
(new Config\ConfigProvider())->register();
(new Router\RouterProvider())->register();
(new OAuth\OAuthProvider())->register();
(new Sessions\SessionsProvider())->register();
(new Boost\BoostProvider())->register();
......@@ -105,6 +106,7 @@ class Minds extends base
(new Faq\FaqProvider())->register();
(new Rewards\RewardsProvider())->register();
(new Plus\PlusProvider())->register();
(new Pro\ProProvider())->register();
(new Hashtags\HashtagsProvider())->register();
(new Feeds\FeedsProvider())->register();
(new Analytics\AnalyticsProvider())->register();
......
<?php
/**
* Manager
* @author edgebal
*/
namespace Minds\Core\Pro\Channel;
use Exception;
use Minds\Core\Data\cache\abstractCacher;
use Minds\Core\Di\Di;
use Minds\Core\Feeds\Top\Manager as TopManager;
use Minds\Core\Pro\Repository;
use Minds\Core\Pro\Settings;
use Minds\Entities\User;
class Manager
{
const CACHE_TTL = 300; // Cache homepage content for 5 minutes
/** @var Repository */
protected $repository;
/** @var TopManager */
protected $top;
/** @var abstractCacher */
protected $cache;
/** @var User */
protected $user;
/**
* Manager constructor.
* @param Repository $repository
* @param TopManager $top
* @param abstractCacher $cache
*/
public function __construct(
$repository = null,
$top = null,
$cache = null
) {
$this->repository = $repository ?: new Repository();
$this->top = $top ?: new TopManager();
$this->cache = $cache ?: Di::_()->get('Cache');
}
/**
* @param User $user
* @return Manager
*/
public function setUser(User $user): Manager
{
$this->user = $user;
return $this;
}
/**
* @return array
* @throws Exception
*/
public function getAllCategoriesContent(): array
{
if (!$this->user) {
throw new Exception('No user set');
}
/** @var Settings $settings */
$settings = $this->repository->getList([
'user_guid' => $this->user->guid
])->first();
if (!$settings) {
throw new Exception('Invalid Pro user');
}
$cacheKey = sprintf("pro::v1::getAllCategoriesContent::%s", $this->user->guid);
$cachedContent = $this->cache->get($cacheKey);
if ($cachedContent) {
return $cachedContent;
}
$tags = $settings->getTagList() ?: [];
$output = [];
$container = (string) $this->user->guid;
foreach ($tags as $tag) {
$opts = [
'container_guid' => $container,
'access_id' => [2, $container],
'hashtags' => [strtolower($tag['tag'])],
'filter_hashtags' => true,
'limit' => 4,
'type' => 'all',
'algorithm' => 'top',
'period' => '7d',
'sync' => true,
'single_owner_threshold' => 0,
];
$content = $this->top->getList($opts)->toArray();
if (count($content) < 2) {
$opts['algorithm'] = 'latest';
$content = $this->top->getList($opts)->toArray();
}
$output[] = [
'tag' => $tag,
'content' => $content,
];
}
$this->cache->set($cacheKey, $output, static::CACHE_TTL);
return $output;
}
}
<?php
/**
* HydrateSettingsDelegate
* @author edgebal
*/
namespace Minds\Core\Pro\Delegates;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\EntitiesBuilder;
use Minds\Core\Pro\Settings;
use Minds\Entities\Object\Carousel;
use Minds\Entities\User;
use Minds\Helpers\Text;
class HydrateSettingsDelegate
{
/** @var EntitiesBuilder */
protected $entitiesBuilder;
/** @var Config */
protected $config;
/**
* HydrateSettingsDelegate constructor.
* @param EntitiesBuilder $entitiesBuilder
* @param Config $config
*/
public function __construct(
$entitiesBuilder = null,
$config = null
) {
$this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder');
$this->config = $config ?: Di::_()->get('Config');
}
/**
* @param User $user
* @param Settings $settings
* @return Settings
*/
public function onGet(User $user, Settings $settings): Settings
{
try {
$logoImage = $settings->getLogoGuid() ? sprintf(
'%sfs/v1/thumbnail/%s/master',
$this->config->get('cdn_url'),
$settings->getLogoGuid()
) : $user->getIconURL('large');
if ($logoImage) {
$settings
->setLogoImage($logoImage);
}
} catch (\Exception $e) {
error_log($e);
}
try {
$carousels = $this->entitiesBuilder->get(['subtype' => 'carousel', 'owner_guid' => (string) $user->guid]);
$carousel = $carousels[0] ?? null;
if ($carousel) {
$settings
->setBackgroundImage(sprintf(
'%sfs/v1/banners/%s/fat/%s',
$this->config->get('cdn_url'),
$carousel->guid,
$carousel->last_updated
));
}
} catch (\Exception $e) {
error_log($e);
}
try {
if ($user->getPinnedPosts()) {
$pinnedPosts = $this->entitiesBuilder->get(['guids' => Text::buildArray($user->getPinnedPosts())]);
uasort($pinnedPosts, function ($a, $b) {
if (!$a || !$b) {
return 0;
}
return ($a->time_created < $b->time_created) ? 1 : -1;
});
$featuredContent = Text::buildArray(array_values(array_filter(array_map(function ($pinnedPost) {
return $pinnedPost->entity_guid ?: $pinnedPost->guid ?: null;
}, $pinnedPosts))));
$settings->setFeaturedContent($featuredContent);
}
} catch (\Exception $e) {
error_log($e);
}
return $settings;
}
}
<?php
/**
* InitializeSettingsDelegate
* @author edgebal
*/
namespace Minds\Core\Pro\Delegates;
use Exception;
use Minds\Core\Pro\Repository;
use Minds\Core\Pro\Settings;
use Minds\Entities\User;
class InitializeSettingsDelegate
{
/** @var Repository */
protected $repository;
/** @var SetupRoutingDelegate */
protected $setupRoutingDelegate;
/**
* InitializeSettingsDelegate constructor.
* @param Repository $repository
* @param SetupRoutingDelegate $setupRoutingDelegate
*/
public function __construct(
$repository = null,
$setupRoutingDelegate = null
) {
$this->repository = $repository ?: new Repository();
$this->setupRoutingDelegate = $setupRoutingDelegate ?: new SetupRoutingDelegate();
}
/**
* @param User $user
* @throws Exception
*/
public function onEnable(User $user): void
{
/** @var Settings|null $settings */
$settings = $this->repository
->getList(['user_guid' => $user->guid])
->first();
if (!$settings) {
$settings = new Settings();
$settings
->setUserGuid($user->guid);
}
if (!$settings->getTitle()) {
$settings->setTitle($user->name ?: $user->username);
}
$this->setupRoutingDelegate
->onUpdate($settings);
$this->repository
->add($settings);
}
}
<?php
/**
* SetupRoutingDelegate
* @author edgebal
*/
namespace Minds\Core\Pro\Delegates;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Domain\EdgeRouters\EdgeRouterInterface;
use Minds\Core\Pro\Domain\EdgeRouters\TraefikDynamoDb;
use Minds\Core\Pro\Settings;
class SetupRoutingDelegate
{
/** @var Config */
protected $config;
/** @var EdgeRouterInterface */
protected $edgeRouter;
/**
* SetupRoutingDelegate constructor.
* @param Config $config
* @param EdgeRouterInterface $edgeRouter
*/
public function __construct(
$config = null,
$edgeRouter = null
) {
$this->config = $config ?: Di::_()->get('Config');
$this->edgeRouter = $edgeRouter ?: new TraefikDynamoDb();
}
/**
* @param Settings $settings
*/
public function onUpdate(Settings $settings): void
{
$userGuid = $settings->getUserGuid();
if (!$settings->getDomain()) {
$settings->setDomain(sprintf("pro-%s.%s", $userGuid, $this->config->get('pro')['subdomain_suffix'] ?? 'minds.com'));
}
$success = $this->edgeRouter
->initialize()
->putEndpoint($settings);
if (!$success) {
error_log("[MindsPro] Cannot setup endpoint.");
// TODO: Implement user-facing warning
}
}
}
<?php
/**
* Domain
* @author edgebal
*/
namespace Minds\Core\Pro;
use Exception;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Entities\User;
class Domain
{
/** @var Config */
protected $config;
/** @var Repository */
protected $repository;
/**
* Domain constructor.
* @param Config $config
* @param Repository $repository
*/
public function __construct(
$config = null,
$repository = null
) {
$this->config = $config ?: Di::_()->get('Config');
$this->repository = $repository ?: new Repository();
}
/**
* @param string $domain
* @return Settings|null
*/
public function lookup(string $domain): ?Settings
{
$rootDomains = $this->config->get('pro')['root_domains'] ?? [];
if (in_array(strtolower($domain), $rootDomains, true)) {
return null;
}
return $this->repository->getList([
'domain' => $domain,
])->first();
}
/**
* @param Settings $settings
* @param User|null $owner
* @return string
* @throws Exception
*/
public function getIcon(Settings $settings, User $owner = null): string
{
if (!$owner) {
$owner = new User();
$owner->guid = $settings->getUserGuid();
}
return $owner->getIconURL('large');
}
}
<?php
/**
* EdgeRouterInterface
* @author edgebal
*/
namespace Minds\Core\Pro\Domain\EdgeRouters;
use Minds\Core\Pro\Settings;
interface EdgeRouterInterface
{
public function initialize(): EdgeRouterInterface;
public function putEndpoint(Settings $settings): bool;
}
<?php
/**
* TraefikDynamoDb
* @author edgebal
*/
namespace Minds\Core\Pro\Domain\EdgeRouters;
use Aws\DynamoDb\DynamoDbClient;
use Aws\DynamoDb\Exception\DynamoDbException;
use Aws\DynamoDb\Marshaler;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Settings;
class TraefikDynamoDb implements EdgeRouterInterface
{
/** @var Config */
protected $config;
/** @var DynamoDbClient */
protected $dynamoDb;
/**
* TraefikDynamoDb constructor.
* @param Config $config
* @param DynamoDbClient $dynamoDb
*/
public function __construct(
$config = null,
$dynamoDb = null
) {
$this->config = $config ?: Di::_()->get('Config');
$this->dynamoDb = $dynamoDb;
}
/**
* @return EdgeRouterInterface
*/
public function initialize(): EdgeRouterInterface
{
$awsConfig = $this->config->get('aws');
$opts = [
'region' => $awsConfig['region']
];
if (!isset($awsConfig['useRoles']) || !$awsConfig['useRoles']) {
$opts['credentials'] = [
'key' => $awsConfig['key'],
'secret' => $awsConfig['secret'],
];
}
if (isset($awsConfig['dynamoDbEndpoint'])) {
$opts['endpoint'] = $awsConfig['dynamoDbEndpoint'];
}
$this->dynamoDb = new DynamoDbClient(array_merge([
'version' => '2012-08-10',
], $opts));
return $this;
}
/**
* @param Settings $settings
* @return bool
*/
public function putEndpoint(Settings $settings): bool
{
$domain = $settings->getDomain();
if (!$domain) {
return false;
}
$userGuid = (string) $settings->getUserGuid();
$marshaler = new Marshaler();
$entry = $marshaler->marshalJson(json_encode([
'id' => "minds-pro-frontend-{$userGuid}",
'name' => "minds-pro-{$userGuid}",
'frontend' => [
'backend' => 'minds-pro',
'routes' => [
'pro-domain' => [
'rule' => "Host: {$domain}"
]
],
'headers' => [
'SSLRedirect' => true,
'customrequestheaders' => [
'X-Minds-Pro' => '1',
],
],
'passHostHeader' => true,
],
]));
try {
$this->dynamoDb->putItem([
'TableName' => $this->config->get('pro')['dynamodb_table_name'],
'Item' => $entry,
]);
return true;
} catch (DynamoDbException $e) {
error_log($e);
return false;
}
}
}
<?php
/**
* Security
* @author edgebal
*/
namespace Minds\Core\Pro\Domain;
use Exception;
use Minds\Common\Cookie;
use Minds\Common\Jwt;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Zend\Diactoros\ServerRequest;
class Security
{
/** @var string */
const JWT_COOKIE_NAME = 'PRO-XSRF-JWT';
/** @var string */
const XSRF_COOKIE_NAME = 'XSRF-TOKEN';
/** @var Cookie */
protected $cookie;
/** @var Jwt */
protected $jwt;
/** @var Config */
protected $config;
/**
* Security constructor.
* @param Cookie $cookie
* @param Jwt $jwt
* @param Config $config
*/
public function __construct(
$cookie = null,
$jwt = null,
$config = null
) {
$this->cookie = $cookie ?: new Cookie();
$this->jwt = $jwt ?: new Jwt();
$this->config = $config ?: Di::_()->get('Config');
}
/**
* @param string $domain
* @return string
* @throws Exception
*/
public function setUp($domain): string
{
$nonce = $this->jwt->randomString();
$nbf = time();
$exp = $nbf + 60;
$jwt = $this->jwt
->setKey($this->getEncryptionKey())
->encode([
'nonce' => $nonce,
], $exp, $nbf);
$this->cookie
->setName(static::JWT_COOKIE_NAME)
->setValue($jwt)
->setExpire($exp)
->setPath('/')
->setHttpOnly(false)
->create();
$this->cookie
->setName(static::XSRF_COOKIE_NAME)
->setValue($nonce)
->setExpire(0)
->setPath('/')
->setHttpOnly(false)
->create();
return $jwt;
}
/**
* @param ServerRequest $request
*/
public function syncCookies(ServerRequest $request): void
{
$jwt = $request->getServerParams()['HTTP_X_PRO_XSRF_JWT'] ?? '';
if (!$jwt) {
return;
}
try {
$data = $this->jwt
->setKey($this->getEncryptionKey())
->decode($jwt);
if (($_COOKIE[static::XSRF_COOKIE_NAME] ?? null) === $data['nonce']) {
return;
}
$this->cookie
->setName(static::XSRF_COOKIE_NAME)
->setValue($data['nonce'])
->setExpire(0)
->setPath('/')
->setHttpOnly(false)
->create();
} catch (Exception $e) {
// Invalid or expired JWT
}
}
/**
* @return string
*/
protected function getEncryptionKey(): string
{
return $this->config->get('oauth')['encryption_key'] ?? '';
}
}
<?php
/**
* Manager
* @author edgebal
*/
namespace Minds\Core\Pro;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Entities\Actions\Save;
use Minds\Core\EntitiesBuilder;
use Minds\Core\Util\StringValidator;
use Minds\Entities\User;
class Manager
{
/** @var Repository */
protected $repository;
/** @var Save */
protected $saveAction;
/** @var EntitiesBuilder */
protected $entitiesBuilder;
/** @var Delegates\InitializeSettingsDelegate */
protected $initializeSettingsDelegate;
/** @var Delegates\HydrateSettingsDelegate */
protected $hydrateSettingsDelegate;
/** @var Delegates\SetupRoutingDelegate */
protected $setupRoutingDelegate;
/** @var User */
protected $user;
/** @var User */
protected $actor;
/**
* Manager constructor.
* @param Repository $repository
* @param Save $saveAction
* @param EntitiesBuilder $entitiesBuilder
* @param Delegates\InitializeSettingsDelegate $initializeSettingsDelegate
* @param Delegates\HydrateSettingsDelegate $hydrateSettingsDelegate
* @param Delegates\SetupRoutingDelegate $setupRoutingDelegate
*/
public function __construct(
$repository = null,
$saveAction = null,
$entitiesBuilder = null,
$initializeSettingsDelegate = null,
$hydrateSettingsDelegate = null,
$setupRoutingDelegate = null
) {
$this->repository = $repository ?: new Repository();
$this->saveAction = $saveAction ?: new Save();
$this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder');
$this->initializeSettingsDelegate = $initializeSettingsDelegate ?: new Delegates\InitializeSettingsDelegate();
$this->hydrateSettingsDelegate = $hydrateSettingsDelegate ?: new Delegates\HydrateSettingsDelegate();
$this->setupRoutingDelegate = $setupRoutingDelegate ?: new Delegates\SetupRoutingDelegate();
}
/**
* @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;
}
/**
* @return bool
* @throws Exception
*/
public function isActive(): bool
{
if (!$this->user) {
throw new Exception('Invalid user');
}
return $this->user->isPro();
}
/**
* @param $until
* @return bool
* @throws Exception
*/
public function enable($until): bool
{
if (!$this->user) {
throw new Exception('Invalid user');
}
$this->user
->setProExpires($until);
$saved = $this->saveAction
->setEntity($this->user)
->save();
$this->initializeSettingsDelegate
->onEnable($this->user);
return (bool) $saved;
}
/**
* @return bool
* @throws Exception
*/
public function disable(): bool
{
if (!$this->user) {
throw new Exception('Invalid user');
}
// TODO: Disable subscription instead, let Pro expire itself at the end of the sub
$this->user
->setProExpires(0);
$saved = $this->saveAction
->setEntity($this->user)
->save();
return (bool) $saved;
}
/**
* @return Settings|null
* @throws Exception
*/
public function get(): ?Settings
{
if (!$this->user) {
throw new Exception('Invalid user');
}
$settings = $this->repository->getList([
'user_guid' => $this->user->guid,
])->first();
if (!$settings) {
return null;
}
return $this->hydrate($settings);
}
/**
* @param Settings $settings
* @return Settings
*/
public function hydrate(Settings $settings): Settings
{
return $this->hydrateSettingsDelegate
->onGet($this->user, $settings);
}
/**
* @param array $values
* @return bool
* @throws Exception
*/
public function set(array $values = []): bool
{
if (!$this->user) {
throw new Exception('Invalid user');
}
$settings = $this->get() ?: new Settings();
$settings
->setUserGuid($this->user->guid);
if (isset($values['domain'])) {
$domain = trim($values['domain']);
if (!StringValidator::isDomain($domain)) {
throw new \Exception('Invalid domain');
}
$settings
->setDomain($domain);
}
if (isset($values['title'])) {
$title = trim($values['title']);
if (strlen($title) > 60) {
throw new \Exception('Title must be 60 characters or less');
}
$settings
->setTitle($title);
}
if (isset($values['headline'])) {
$headline = trim($values['headline']);
if (strlen($headline) > 80) {
throw new \Exception('Headline must be 80 characters or less');
}
$settings
->setHeadline($headline);
}
if (isset($values['text_color'])) {
if (!StringValidator::isHexColor($values['text_color'])) {
throw new \Exception('Text color must be a valid hex color');
}
$settings
->setTextColor($values['text_color']);
}
if (isset($values['primary_color'])) {
if (!StringValidator::isHexColor($values['primary_color'])) {
throw new \Exception('Primary color must be a valid hex color');
}
$settings
->setPrimaryColor($values['primary_color']);
}
if (isset($values['plain_background_color'])) {
if (!StringValidator::isHexColor($values['plain_background_color'])) {
throw new \Exception('Plain background color must be a valid hex color');
}
$settings
->setPlainBackgroundColor($values['plain_background_color']);
}
if (isset($values['tile_ratio'])) {
if (!in_array($values['tile_ratio'], Settings::TILE_RATIOS, true)) {
throw new \Exception('Invalid tile ratio');
}
$settings
->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']);
if (strlen($footer_text) > 80) {
throw new \Exception('Footer text must be 80 characters or less');
}
$settings
->setFooterText($footer_text);
}
if (isset($values['footer_links']) && is_array($values['footer_links'])) {
$footerLinks = array_map(function ($item) {
$href = $item['href'];
$title = ($item['title'] ?? null) ?: $item['href'];
return compact('title', 'href');
}, array_filter($values['footer_links'], function ($item) {
return $item && $item['href'] && filter_var($item['href'], FILTER_VALIDATE_URL);
}));
$settings
->setFooterLinks(array_values($footerLinks));
}
if (isset($values['tag_list']) && is_array($values['tag_list'])) {
$tagList = array_map(function ($item) {
$tag = trim($item['tag'], "#\t\n\r");
$label = ($item['label'] ?? null) ?: "#{$item['tag']}";
return compact('label', 'tag');
}, array_filter($values['tag_list'], function ($item) {
return $item && $item['tag'];
}));
$settings
->setTagList(array_values($tagList));
}
if (isset($values['scheme'])) {
if (!in_array($values['scheme'], Settings::COLOR_SCHEMES, true)) {
throw new \Exception('Invalid tile ratio');
}
$settings
->setScheme($values['scheme']);
}
if (isset($values['custom_head']) && $this->actor->isAdmin()) {
$settings
->setCustomHead($values['custom_head']);
}
$this->setupRoutingDelegate
->onUpdate($settings);
return $this->repository->update($settings);
}
}
<?php
/**
* ProProvider
* @author edgebal
*/
namespace Minds\Core\Pro;
use Minds\Core\Di\ImmutableException;
use Minds\Core\Di\Provider;
class ProProvider extends Provider
{
/**
* @throws ImmutableException
*/
public function register()
{
$this->di->bind('Pro\Manager', function ($di) {
return new Manager();
}, ['useFactory' => true]);
$this->di->bind('Pro\Domain', function ($di) {
return new Domain();
}, ['useFactory' => true]);
$this->di->bind('Pro\Domain\Security', function ($di) {
return new Domain\Security();
}, ['useFactory' => true]);
$this->di->bind('Pro\Domain\Subscription', function ($di) {
return new Domain\Subscription();
}, ['useFactory' => true]);
$this->di->bind('Pro\SEO', function ($di) {
return new SEO();
}, ['useFactory' => true]);
$this->di->bind('Pro\Channel\Manager', function ($di) {
return new Channel\Manager();
}, ['useFactory' => true]);
}
}
<?php
/**
* Repository
* @author edgebal
*/
namespace Minds\Core\Pro;
use Cassandra\Bigint;
use Cassandra\Rows;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Data\Cassandra\Client;
use Minds\Core\Data\Cassandra\Prepared\Custom;
use Minds\Core\Di\Di;
class Repository
{
/** @var Client */
protected $db;
/**
* Repository constructor.
* @param Client $db
*/
public function __construct(
$db = null
) {
$this->db = $db ?: Di::_()->get('Database\Cassandra\Cql');
}
/**
* @param array $opts
* @return Response
*/
public function getList(array $opts = []): Response
{
$opts = array_merge([
'user_guid' => null,
'domain' => null,
'limit' => null,
'offset' => null,
], $opts);
$cql = "SELECT * FROM pro";
$where = [];
$values = [];
$cqlOpts = [];
if ($opts['user_guid']) {
$where[] = 'user_guid = ?';
$values[] = new Bigint($opts['user_guid']);
} elseif ($opts['domain']) {
$cql = "SELECT * FROM pro_by_domain";
$where[] = 'domain = ?';
$values[] = $opts['domain'];
}
if ($where) {
$cql .= sprintf(" WHERE %s", implode(' AND ', $where));
}
if ($opts['limit']) {
$cqlOpts['page_size'] = (int) $opts['limit'];
}
if ($opts['offset']) {
$cqlOpts['paging_state_token'] = base64_decode($opts['offset'], true);
}
$prepared = new Custom();
$prepared->query($cql, $values);
$prepared->setOpts($cqlOpts);
$response = new Response();
try {
/** @var Rows $rows */
$rows = $this->db->request($prepared);
if ($rows) {
foreach ($rows as $row) {
$settings = new Settings();
$settings
->setUserGuid($row['user_guid']->toInt())
->setDomain($row['domain']);
$data = json_decode($row['json_data'] ?: '{}', true);
$settings
->setTitle($data['title'] ?? '')
->setHeadline($data['headline'] ?? '')
->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'] ?? '')
;
$response[] = $settings;
}
$response
->setLastPage($rows->isLastPage())
->setPagingToken(base64_encode($rows->pagingStateToken()));
}
} catch (Exception $e) {
error_log("[ProRepository] $e");
$response->setException($e);
}
return $response;
}
/**
* @param Settings $settings
* @return bool
* @throws Exception
*/
public function add(Settings $settings): bool
{
if (!$settings->getUserGuid()) {
throw new Exception('Invalid user GUID');
}
$cql = "INSERT INTO pro (user_guid, domain, json_data) VALUES (?, ?, ?)";
$settings = [
new Bigint($settings->getUserGuid()),
$settings->getDomain(),
json_encode([
'user_guid' => (string) $settings->getUserGuid(),
'domain' => $settings->getDomain(),
'title' => $settings->getTitle(),
'headline' => $settings->getHeadline(),
'text_color' => $settings->getTextColor(),
'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(),
]),
];
$prepared = new Custom();
$prepared->query($cql, $settings);
return (bool) $this->db->request($prepared, true);
}
/**
* @param Settings $settings
* @return bool
* @throws Exception
*/
public function update(Settings $settings): bool
{
return $this->add($settings);
}
/**
* @param Settings $settingsRef
* @return bool
* @throws Exception
*/
public function delete(Settings $settingsRef): bool
{
if (!$settingsRef->getUserGuid()) {
throw new Exception('Invalid user GUID');
}
$cql = "DELETE FROM pro WHERE user_guid = ?";
$settingsRef = [
new Bigint($settingsRef->getUserGuid()),
];
$prepared = new Custom();
$prepared->query($cql, $settingsRef);
return (bool) $this->db->request($prepared, true);
}
}
<?php
/**
* @author: eiennohi.
*/
namespace Minds\Core\Pro;
use Exception;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\EntitiesBuilder;
use Minds\Core\SEO\Manager;
use Minds\Entities\Activity;
use Minds\Entities\User;
use Minds\Helpers;
class SEO
{
/** @var EntitiesBuilder */
protected $entitiesBuilder;
/** @var Config */
protected $config;
/** @var User */
protected $user;
/**
* SEO constructor.
* @param EntitiesBuilder $entitiesBuilder
* @param Config $config
*/
public function __construct(EntitiesBuilder $entitiesBuilder = null, Config $config = null)
{
$this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder');
$this->config = $config ?: Di::_()->get('Config');
}
/**
* @param User $user
* @return SEO
*/
public function setUser(User $user): SEO
{
$this->user = $user;
return $this;
}
/**
* @param Settings $proSettings
* @throws Exception
*/
public function setup(Settings $proSettings): void
{
Manager::reset();
$title = $proSettings->getTitle() ?: $this->user->name;
$tagList = array_map(function ($tag) {
return $tag['tag'];
}, $proSettings->getTagList());
Manager::setDefaults([
'title' => $title,
'description' => $proSettings->getOneLineHeadline(),
'keywords' => implode(',', $tagList),
'og:title' => $title,
'og:url' => $proSettings->getDomain(),
'og:description' => $proSettings->getOneLineHeadline(),
'og:type' => 'website',
'og:image' => $this->user->getIconURL('large'),
]);
Manager::add('/', function () {
});
Manager::add('/newsfeed', [$this, 'activityHandler']);
Manager::add('/media', [$this, 'entityHandler']);
// blog route added in Blogs\SEO
}
/**
* @param array $slugs
* @return array|null
*/
public function activityHandler($slugs = []): ?array
{
if (!isset($slugs[0]) || !is_numeric($slugs[0])) {
return null;
}
$activity = new Activity($slugs[0]);
if (!$activity->guid || Helpers\Flags::shouldFail($activity)) {
header("HTTP/1.0 404 Not Found");
return [
'robots' => 'noindex',
];
}
if ($activity->paywall) {
return null;
}
$title = $activity->title ?: $activity->message;
$description = $activity->blurb ?: "@{$activity->ownerObj['username']} on {$this->config->site_name}";
$meta = [
'title' => $title,
'description' => $description,
'og:title' => $title,
'og:description' => $description,
'og:url' => $activity->getUrl(),
'og:image' => $activity->custom_type == 'batch' ? $activity->custom_data[0]['src'] : $activity->thumbnail_src,
'og:image:width' => 2000,
'og:image:height' => 1000,
'twitter:site' => '@minds',
'twitter:card' => 'summary',
'al:ios:url' => 'minds://activity/' . $activity->guid,
'al:android:url' => 'minds://minds/activity/' . $activity->guid,
'robots' => 'all',
];
if ($activity->custom_type == 'video') {
$meta['og:type'] = "video";
$meta['og:image'] = $activity->custom_data['thumbnail_src'];
}
return $meta;
}
/**
* @param $entity
* @param $prop
* @return mixed
*/
public function getEntityProperty($entity, $prop)
{
$getter = "get${$prop}";
if (isset($entity->{$prop})) {
return $entity->{$prop};
} elseif (Helpers\MagicAttributes::getterExists($entity, $getter)) {
return $entity->{$getter}();
}
return null;
}
/**
* @param array $slugs
* @return array|null
*/
public function entityHandler($slugs = []): ?array
{
if (!isset($slugs[0]) || !is_numeric($slugs[0])) {
return null;
}
$entity = $this->entitiesBuilder->single($slugs[0]);
if (!$entity->guid || Helpers\Flags::shouldFail($entity)) {
header("HTTP/1.0 404 Not Found");
return [
'robots' => 'noindex',
];
}
if ($entity->paywall) {
return null;
}
$owner = $this->getEntityProperty($entity, 'ownerObj');
$title = $this->getEntityProperty($entity, 'title') ?: $this->getEntityProperty($entity, 'description');
$siteName = $this->config->site_name;
$description = $title ?? $this->getEntityProperty($entity, 'blurb') ?? "@{$owner['username']} on {$siteName}";
$meta = [
'title' => $title,
'description' => $description,
'og:title' => $title,
'og:description' => $description,
'og:url' => $entity->getUrl(),
'og:image:width' => 2000,
'og:image:height' => 1000,
'robots' => 'all',
];
switch ($entity->subtype) {
case 'video':
$meta['og:type'] = "video";
$meta['og:image'] = $entity->getIconUrl();
break;
case 'image':
$meta['og:type'] = "image";
$meta['og:image'] = $entity->getIconUrl();
break;
case 'blog':
$meta['og:type'] = "blog";
$meta['og:image'] = $entity->getIconUrl();
break;
case 'group':
$meta['og:type'] = "group";
$meta['og:image'] = $this->config->cdn_url . 'fs/v1/banner/' . $entity->banner;
break;
}
return $meta;
}
}
<?php
/**
* Settings
* @author edgebal
*/
namespace Minds\Core\Pro;
use JsonSerializable;
use Minds\Traits\MagicAttributes;
/**
* Class Settings
* @package Minds\Core\Pro
* @method int|string getUserGuid()
* @method Settings setUserGuid(int|string $userGuid)
* @method string getDomain()
* @method Settings setDomain(string $domain)
* @method string getTitle()
* @method Settings setTitle(string $title)
* @method string getHeadline()
* @method Settings setHeadline(string $headline)
* @method string getTextColor()
* @method Settings setTextColor(string $textColor)
* @method string getPrimaryColor()
* @method Settings setPrimaryColor(string $primaryColor)
* @method string getPlainBackgroundColor()
* @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()
* @method Settings setFooterLinks(array $footerLinks)
* @method array getTagList()
* @method Settings setTagList(array $tagList)
* @method string getScheme()
* @method Settings setScheme(string $scheme)
* @method string getBackgroundImage()
* @method Settings setBackgroundImage(string $backgroundImage)
* @method string getLogoImage()
* @method Settings setLogoImage(string $logoImage)
* @method array getFeaturedContent()
* @method Settings setFeaturedContent(array $featuredContent)
* @method string getCustomHead()
* @method Settings setCustomHead(string $customHead)
*/
class Settings implements JsonSerializable
{
use MagicAttributes;
/** @var string */
const DEFAULT_TEXT_COLOR = '#000000';
/** @var string */
const DEFAULT_PRIMARY_COLOR = '#4690df';
/** @var string */
const DEFAULT_PLAIN_BACKGROUND_COLOR = '#ffffff';
/** @var string */
const DEFAULT_TILE_RATIO = '16:9';
/** @var array */
const TILE_RATIOS = ['16:9', '16:10', '4:3', '1:1'];
/** @var array */
const COLOR_SCHEMES = ['light', 'dark'];
/** @var int */
protected $userGuid;
/** @var string */
protected $domain;
/** @var string */
protected $title;
/** @var string */
protected $headline;
/** @var string */
protected $textColor;
/** @var string */
protected $primaryColor;
/** @var string */
protected $plainBackgroundColor;
/** @var int */
protected $logoGuid;
/** @var string */
protected $backgroundImage;
/** @var string */
protected $tileRatio = '16:9';
/** @var string */
protected $logoImage;
/** @var string */
protected $footerText;
/** @var array */
protected $footerLinks = [];
/** @var array */
protected $tagList = [];
/** @var string */
protected $scheme;
/** @var array */
protected $featuredContent = [];
/** @var string */
protected $customHead = '';
/**
* @return string
*/
public function getOneLineHeadline(): string
{
return preg_replace("/\\r?\\n+/", ' ', $this->headline);
}
/**
* @return array
*/
public function export(): array
{
$textColor = $this->textColor ?: static::DEFAULT_TEXT_COLOR;
$primaryColor = $this->primaryColor ?: static::DEFAULT_PRIMARY_COLOR;
$plainBackgroundColor = $this->plainBackgroundColor ?: static::DEFAULT_PLAIN_BACKGROUND_COLOR;
$tileRatio = $this->tileRatio ?: static::DEFAULT_TILE_RATIO;
return [
'user_guid' => (string) $this->userGuid,
'domain' => $this->domain,
'title' => $this->title,
'headline' => $this->headline,
'text_color' => $textColor,
'primary_color' => $primaryColor,
'plain_background_color' => $plainBackgroundColor,
'tile_ratio' => $tileRatio,
'footer_text' => $this->footerText,
'footer_links' => $this->footerLinks,
'tag_list' => $this->tagList,
'logo_guid' => (string) $this->logoGuid,
'background_image' => $this->backgroundImage,
'logo_image' => $this->logoImage,
'featured_content' => $this->featuredContent,
'scheme' => $this->scheme,
'custom_head' => $this->customHead,
'one_line_headline' => $this->getOneLineHeadline(),
'styles' => $this->buildStyles(),
];
}
/**
* @return array
*/
public function buildStyles(): array
{
$textColor = $this->textColor ?: static::DEFAULT_TEXT_COLOR;
$primaryColor = $this->primaryColor ?: static::DEFAULT_PRIMARY_COLOR;
$plainBackgroundColor = $this->plainBackgroundColor ?: static::DEFAULT_PLAIN_BACKGROUND_COLOR;
$tileRatioPercentage = $this->calcTileRatioPercentage();
return [
'text_color' => $textColor,
'primary_color' => $primaryColor,
'plain_background_color' => $plainBackgroundColor,
'transparent_background_color' => sprintf("%sa0", $plainBackgroundColor),
'more_transparent_background_color' => sprintf("%s50", $plainBackgroundColor),
'tile_ratio' => sprintf("%s%%", $tileRatioPercentage),
];
}
/**
* @return float
*/
public function calcTileRatioPercentage(): float
{
$ratioFragments = explode(':', $this->tileRatio ?: '16:9');
$percentage = $ratioFragments[1] / $ratioFragments[0] * 100;
return round($percentage, 3);
}
/**
* Specify data which should be serialized to JSON
* @link https://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize(): array
{
return $this->export();
}
}
......@@ -1446,4 +1446,18 @@ CREATE TABLE minds.email_campaign_logs (
PRIMARY KEY (receiver_guid, time_sent)
) WITH CLUSTERING ORDER BY (time_sent desc);
ALTER TABLE minds.views ADD owner_guid text;
\ No newline at end of file
ALTER TABLE minds.views ADD owner_guid text;
CREATE TABLE minds.pro (
user_guid bigint,
domain text,
json_data text,
PRIMARY KEY (user_guid)
);
CREATE MATERIALIZED VIEW minds.pro_by_domain AS
SELECT domain, user_guid, json_data
FROM minds.pro
WHERE user_guid IS NOT NULL AND domain IS NOT NULL
PRIMARY KEY (domain, user_guid)
WITH CLUSTERING ORDER BY (user_guid ASC);
<?php
namespace Minds\Core\Queue\Runners;
use Minds\Core\Di\Di;
......@@ -28,7 +27,6 @@ class Registered implements QueueRunner
//subscribe to minds channel
$subscriber = new User($user_guid);
$subscriber->subscribe('100000000000000519');
echo "[registered]: User registered $user_guid\n";
......
......@@ -14,5 +14,8 @@ class Provider extends DiProvider
$this->di->bind('Referrals\Manager', function ($di) {
return new Manager();
}, [ 'useFactory'=>false ]);
$this->di->bind('Referrals\Cookie', function ($di) {
return new ReferralCookie();
}, [ 'useFactory'=>true ]);
}
}
<?php
/**
* Referral Cookie
*/
namespace Minds\Core\Referrals;
use Minds\Entities\User;
use Minds\Common\Cookie;
use Zend\Diactoros\ServerRequest;
class ReferralCookie
{
/** @var Request */
private $request;
/** @var Entity */
private $entity;
/**
* Set the router request
* @param Request $request
* @param Response $response
* @return $this
*/
public function withRouterRequest(ServerRequest $request): ReferralCookie
{
$this->request = $request;
return $this;
}
/**
* Set Entity
* @param Entity|User $entity
* @return $this
*/
public function setEntity($entity): ReferralCookie
{
$this->entity = $entity;
return $this;
}
/**
* Set the referral cookie
* @return void
*/
public function create(): void
{
if (!$this->request) {
return;
}
$cookies = $this->request->getCookieParams();
$params = $this->request->getQueryParams();
if (isset($cookies['referrer'])) {
return; // Do not override previosuly set cookie
}
$referrerGuid = null;
if (isset($params['referrer'])) { // Is a referrer param set in the request?
$referrerGuid = $params['referrer'];
} elseif ($this->entity) { // Was an entity set?
switch (get_class($this->entity)) {
case User::class:
$referrerGuid = $this->entity->getGuid();
break;
default:
$referrerGuid = $this->entity->getOwnerGuid();
}
}
if ($referrerGuid) {
$cookie = new Cookie();
$cookie
->setName('referrer')
->setValue($referrerGuid)
->setExpire(time() + (60 * 60 * 24)) //valid for 24 hours
->setPath('/')
->create();
$_COOKIE['referrer'] = $referrerGuid; // TODO: replace with Response object later
}
}
}
......@@ -2,12 +2,12 @@
namespace Minds\Core;
use Minds\Core\Di\Di;
use Minds\Core\I18n\I18n;
use Minds\Core\Router\Manager;
use Minds\Helpers;
use Minds\Core\Di\Di;
use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequestFactory;
/**
* Minds Core Router.
......@@ -36,6 +36,20 @@ class Router
'/checkout' => '\\Minds\\Controllers\\checkout',
];
/** @var Manager */
protected $manager;
/**
* Router constructor.
* @param Manager $manager
*/
public function __construct(
$manager = null
) {
/** @var Router\Manager $manager */
$this->manager = $manager ?: Di::_()->get('Router\Manager');
}
/**
* Route the pages
* (fallback to elgg page handler if we fail).
......@@ -70,6 +84,13 @@ class Router
$request = ServerRequestFactory::fromGlobals();
$response = new JsonResponse([]);
$result = $this->manager
->handle($request, $response);
if ($result === false) {
return null;
}
if ($request->getMethod() === 'OPTIONS') {
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
......@@ -95,8 +116,6 @@ class Router
// XSRF Cookie - may be able to remove now with OAuth flow
Security\XSRF::setCookie();
new SEO\Defaults(Di::_()->get('Config'));
if (Session::isLoggedin()) {
Helpers\Analytics::increment('active');
}
......@@ -109,9 +128,10 @@ class Router
Di::_()->get('Email\RouterHooks')
->withRouterRequest($request);
if (isset($_GET['referrer'])) {
Helpers\Campaigns\Referrals::register($_GET['referrer']);
}
Di::_()->get('Referrals\Cookie')
->withRouterRequest($request)
->create();
$loop = count($segments);
while ($loop >= 0) {
$offset = $loop - 1;
......
<?php
/**
* Manager
* @author edgebal
*/
namespace Minds\Core\Router;
use Minds\Core\Router\Middleware;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequest;
class Manager
{
/** @var Middleware\RouterMiddleware[] */
protected $middleware;
/**
* Manager constructor.
* @param Middleware\RouterMiddleware[] $middleware
*/
public function __construct(
$middleware = null
) {
$this->middleware = $middleware ?: [
new Middleware\SEOMiddleware(),
new Middleware\ProMiddleware(), // this needs to always be the last element in this array
];
}
/**
* @param ServerRequest $request
* @param JsonResponse $response
* @return bool|null
*/
public function handle(ServerRequest &$request, JsonResponse &$response): ?bool
{
$result = null;
foreach ($this->middleware as $middleware) {
$result = $middleware->onRequest($request, $response);
if ($result === false) {
break;
}
}
return $result;
}
}
<?php
/**
* ProMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware;
use Exception;
use Minds\Core\EntitiesBuilder;
use Minds\Core\Pro\Manager;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Domain;
use Minds\Core\Pro\SEO;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequest;
class ProMiddleware implements RouterMiddleware
{
/** @var Domain */
protected $domain;
/** @var Domain\Security */
protected $domainSecurity;
/** @var Manager */
protected $manager;
/** @var SEO */
protected $seo;
/** @var EntitiesBuilder */
protected $entitiesBuilder;
/**
* ProMiddleware constructor.
* @param Domain $domain
* @param Domain\Security $domainSecurity
* @param Manager $manager
* @param SEO $seo
* @param EntitiesBuilder $entitiesBuilder
*/
public function __construct(
$domain = null,
$domainSecurity = null,
$manager = null,
$seo = null,
$entitiesBuilder = null
) {
$this->domain = $domain ?: Di::_()->get('Pro\Domain');
$this->domainSecurity = $domainSecurity ?: Di::_()->get('Pro\Domain\Security');
$this->manager = $manager ?: Di::_()->get('Pro\Manager');
$this->seo = $seo ?: Di::_()->get('Pro\SEO');
$this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder');
}
/**
* @param ServerRequest $request
* @param JsonResponse $response
* @return bool|null
* @throws Exception
*/
public function onRequest(ServerRequest $request, JsonResponse &$response): ?bool
{
$serverParams = $request->getServerParams() ?? [];
$originalHost = $serverParams['HTTP_HOST'];
$scheme = $request->getUri()->getScheme();
$host = parse_url($serverParams['HTTP_ORIGIN'] ?? '', PHP_URL_HOST) ?: $originalHost;
if (!$host) {
return null;
}
$settings = $this->domain->lookup($host);
if (!$settings) {
return null;
}
header(sprintf("Access-Control-Allow-Origin: %s://%s", $scheme, $host));
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,X-No-Cache,x-xsrf-token,x-pro-xsrf-jwt,x-minds-origin,x-version');
if ($request->getMethod() === 'OPTIONS') {
return false;
}
// Get Pro channel
$user = $this->entitiesBuilder->single($settings->getUserGuid());
// Hydrate with asset URLs
$settings = $this->manager
->setUser($user)
->hydrate($settings);
// Setup SEO
$this->seo
->setUser($user)
->setup($settings);
// Initialize XRSF JWT cookie, only if we're on Pro domain's scope
// If not and within 1 minute, update XSRF cookie to match it
if ($originalHost === $settings->getDomain()) {
$this->domainSecurity
->setUp($settings->getDomain());
} else {
$this->domainSecurity
->syncCookies($request);
}
return null;
}
}
<?php
/**
* RouterMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequest;
interface RouterMiddleware
{
/**
* @param ServerRequest $request
* @param JsonResponse $response
* @return bool|null
*/
public function onRequest(ServerRequest $request, JsonResponse &$response): ?bool;
}
<?php
namespace Minds\Core\Router\Middleware;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\SEO;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequest;
class SEOMiddleware implements RouterMiddleware
{
/** @var Config */
protected $config;
/**
* SEOMiddleware constructor.
* @param Config $config
*/
public function __construct(
$config = null
) {
$this->config = $config ?: Di::_()->get('Config');
}
/**
* @param ServerRequest $request
* @param JsonResponse $response
* @return bool|null
*/
public function onRequest(ServerRequest $request, JsonResponse &$response): ?bool
{
new SEO\Defaults($this->config);
return null;
}
}
<?php
/**
* RouterProvider
* @author edgebal
*/
namespace Minds\Core\Router;
use Minds\Core\Di\Provider;
class RouterProvider extends Provider
{
public function register()
{
$this->di->bind('Router\Manager', function ($di) {
return new Manager();
}, [ 'useFactory' => true ]);
}
}
......@@ -2,6 +2,7 @@
/**
* SEO Manager
*/
namespace Minds\Core\SEO;
use Minds\Core\Events\Dispatcher;
......@@ -10,9 +11,15 @@ class Manager
{
public static $routes = [];
public static $defaults = [
'title' => ''
'title' => '',
];
public static function reset()
{
self::$routes = [];
self::$defaults = ['title' => ''];
}
/**
* Add a callback to provide metadata
* @param string $route
......@@ -49,9 +56,9 @@ class Manager
while ($route) {
$event = Dispatcher::trigger('seo:route', $route, [
'route' => $route,
'slugs' => array_reverse($slugs)
], false);
'route' => $route,
'slugs' => array_reverse($slugs),
], false);
if ($event !== false) {
$meta = $event;
......@@ -62,7 +69,7 @@ class Manager
$meta = call_user_func_array(self::$routes[$route], [array_reverse($slugs)]) ?: [];
break;
} else {
$slugs[] = substr($route, strrpos($route, '/')+1);
$slugs[] = substr($route, strrpos($route, '/') + 1);
if (strrpos($route, '/') === 0) {
$route = '/';
} else {
......
<?php
/**
* @author: eiennohi.
*/
namespace Minds\Core\Util;
class StringValidator
{
protected const DOMAIN_REGEX = '/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/';
protected const HEX_COLOR_REGEX = '/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/';
public static function isDomain(string $domain): bool
{
preg_match(self::DOMAIN_REGEX, $domain, $matches);
return $matches && $matches[0] === $domain;
}
public static function isHexColor(string $hexColor): bool
{
preg_match(self::HEX_COLOR_REGEX, $hexColor, $matches);
return $matches && count($matches) > 0;
}
}
......@@ -31,6 +31,7 @@ class User extends \ElggUser
$this->attributes['tags'] = [];
$this->attributes['plus'] = 0; //TODO: REMOVE
$this->attributes['plus_expires'] = 0;
$this->attributes['pro_expires'] = 0;
$this->attributes['verified'] = 0;
$this->attributes['founder'] = 0;
$this->attributes['disabled_boost'] = 0;
......@@ -449,16 +450,16 @@ class User extends \ElggUser
public function addPinned($guid)
{
$pinned = $this->getPinnedPosts();
if (!$pinned) {
$pinned = [];
} elseif (count($pinned) > 2) {
array_shift($pinned);
}
if (array_search($guid, $pinned, true) === false) {
$pinned[] = (string) $guid;
$this->setPinnedPosts($pinned);
}
$this->setPinnedPosts($pinned);
}
/**
......@@ -489,10 +490,10 @@ class User extends \ElggUser
*/
public function setPinnedPosts($pinned)
{
if (count($pinned) > 3) {
$pinned = array_slice($pinned, 0, 3);
}
$this->pinned_posts = $pinned;
$maxPinnedPosts = $this->isPro() ? 12 : 3;
$this->pinned_posts = array_slice($pinned, -$maxPinnedPosts, null, false);
return $this;
}
......@@ -822,6 +823,7 @@ class User extends \ElggUser
$export['merchant'] = $this->getMerchant() ?: false;
$export['programs'] = $this->getPrograms();
$export['plus'] = (bool) $this->isPlus();
$export['pro'] = (bool) $this->isPro();
$export['verified'] = (bool) $this->verified;
$export['founder'] = (bool) $this->founder;
$export['disabled_boost'] = (bool) $this->disabled_boost;
......@@ -917,6 +919,32 @@ class User extends \ElggUser
return $this;
}
/**
* @param int $proExpires
* @return User
*/
public function setProExpires($proExpires)
{
$this->pro_expires = $proExpires;
return $this;
}
/**
* @return int
*/
public function getProExpires()
{
return $this->pro_expires ?: 0;
}
/**
* @return bool
*/
public function isPro()
{
return $this->getProExpires() >= time();
}
/**
* Gets the categories to which the user is subscribed.
*
......
<?php
namespace Minds\Helpers\Campaigns;
use Minds\Core;
use Minds\Core\Guid;
use Minds\Common\Cookie;
use Minds\Core\Di\Di;
class Referrals
{
/**
* Registers a cookie for the referral step
* @param string $username
* @return null
* @return void
*/
public static function register($username)
public static function register($username): void
{
if (!isset($_COOKIE['referrer'])) {
$cookie = new Cookie();
$cookie
->setName('referrer')
->setValue($username)
->setExpire(time() + (60 * 60 * 24)) //valid for 24 hours
->setPath('/')
->create();
$_COOKIE['referrer'] = $username;
}
Di::_()->get('Referrals\Cookie')->create();
}
}
<?php
namespace Spec\Minds\Core\Pro\Channel;
use Minds\Common\Repository\Response;
use Minds\Core\Data\cache\abstractCacher;
use Minds\Core\Feeds\Top\Manager as TopManager;
use Minds\Core\Pro\Channel\Manager;
use Minds\Core\Pro\Repository;
use Minds\Core\Pro\Settings;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ManagerSpec extends ObjectBehavior
{
/** @var Repository */
protected $repository;
/** @var TopManager */
protected $top;
/** @var abstractCacher */
protected $cache;
public function let(
Repository $repository,
TopManager $top,
abstractCacher $cache
) {
$this->repository = $repository;
$this->top = $top;
$this->cache = $cache;
$this->beConstructedWith($repository, $top, $cache);
}
public function it_is_initializable()
{
$this->shouldHaveType(Manager::class);
}
public function it_should_get_all_categories_content(
User $user,
Response $getListResponse,
Response $topGetListResponse1,
Response $topGetListResponse2Top,
Response $topGetListResponse2Latest,
Settings $settings
) {
$user->get('guid')
->shouldBeCalled()
->willReturn(1000);
$this->repository->getList([
'user_guid' => 1000
])
->shouldBeCalled()
->willReturn($getListResponse);
$getListResponse->first()
->shouldBeCalled()
->willReturn($settings);
$this->cache->get(Argument::containingString('::1000'))
->shouldBeCalled()
->willReturn(null);
$settings->getTagList()
->shouldBeCalled()
->willReturn([
['tag' => 'test1', 'label' => 'Test 1'],
['tag' => 'test2', 'label' => 'Test 2'],
]);
$this->top->getList(Argument::that(function (array $opts) {
return $opts['algorithm'] === 'top' && $opts['hashtags'] === ['test1'];
}))
->shouldBeCalled()
->willReturn($topGetListResponse1);
$topGetListResponse1->toArray()
->shouldBeCalled()
->willReturn([5000, 5001, 5002]);
$this->top->getList(Argument::that(function (array $opts) {
return $opts['algorithm'] === 'top' && $opts['hashtags'] === ['test2'];
}))
->shouldBeCalled()
->willReturn($topGetListResponse2Top);
$topGetListResponse2Top->toArray()
->shouldBeCalled()
->willReturn([]);
$this->top->getList(Argument::that(function (array $opts) {
return $opts['algorithm'] === 'latest' && $opts['hashtags'] === ['test2'];
}))
->shouldBeCalled()
->willReturn($topGetListResponse2Latest);
$topGetListResponse2Latest->toArray()
->shouldBeCalled()
->willReturn([5100, 5101, 5102]);
$output = [
[
'tag' => ['tag' => 'test1', 'label' => 'Test 1'],
'content' => [5000, 5001, 5002],
],
[
'tag' => ['tag' => 'test2', 'label' => 'Test 2'],
'content' => [5100, 5101, 5102],
],
];
$this->cache->set(Argument::containingString('::1000'), $output, Argument::type('int'))
->shouldBeCalled()
->willReturn(true);
$this
->setUser($user)
->getAllCategoriesContent()
->shouldReturn($output);
}
}
<?php
namespace Spec\Minds\Core\Pro\Delegates;
use Exception;
use Minds\Core\Config;
use Minds\Core\EntitiesBuilder;
use Minds\Core\Pro\Delegates\HydrateSettingsDelegate;
use Minds\Core\Pro\Settings;
use Minds\Entities\Activity;
use Minds\Entities\Object\Carousel;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class HydrateSettingsDelegateSpec extends ObjectBehavior
{
/** @var EntitiesBuilder */
protected $entitiesBuilder;
/** @var Config */
protected $config;
public function let(
EntitiesBuilder $entitiesBuilder,
Config $config
) {
$this->entitiesBuilder = $entitiesBuilder;
$this->config = $config;
$this->beConstructedWith($entitiesBuilder, $config);
}
public function it_is_initializable()
{
$this->shouldHaveType(HydrateSettingsDelegate::class);
}
public function it_should_hydrate_settings_on_get(
User $user,
Settings $settings,
Carousel $carousel,
Activity $activity1,
Activity $activity2
) {
$settings->getLogoGuid()
->shouldBeCalled()
->willReturn(7500);
$this->config->get('cdn_url')
->shouldBeCalled()
->willReturn('http://phpspec.test/');
$settings->setLogoImage('http://phpspec.test/fs/v1/thumbnail/7500/master')
->shouldBeCalled()
->willReturn($settings);
$user->get('guid')
->shouldBeCalled()
->willReturn(1000);
$this->entitiesBuilder->get([
'subtype' => 'carousel',
'owner_guid' => '1000'
])
->shouldBeCalled()
->willReturn([ $carousel ]);
$carousel->get('guid')
->shouldBeCalled()
->willReturn(9500);
$carousel->get('last_updated')
->shouldBeCalled()
->willReturn(9999999);
$settings->setBackgroundImage('http://phpspec.test/fs/v1/banners/9500/fat/9999999')
->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);
$this
->shouldNotThrow(Exception::class)
->duringOnGet($user, $settings);
}
}
<?php
namespace Spec\Minds\Core\Pro\Delegates;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Pro\Delegates\InitializeSettingsDelegate;
use Minds\Core\Pro\Delegates\SetupRoutingDelegate;
use Minds\Core\Pro\Repository;
use Minds\Core\Pro\Settings;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class InitializeSettingsDelegateSpec extends ObjectBehavior
{
/** @var Repository */
protected $repository;
/** @var SetupRoutingDelegate */
protected $setupRoutingDelegate;
public function let(
Repository $repository,
SetupRoutingDelegate $setupRoutingDelegate
) {
$this->repository = $repository;
$this->setupRoutingDelegate = $setupRoutingDelegate;
$this->beConstructedWith($repository, $setupRoutingDelegate);
}
public function it_is_initializable()
{
$this->shouldHaveType(InitializeSettingsDelegate::class);
}
public function it_should_initialize_settings_on_enable(
User $user,
Response $getListResponse,
Settings $settings
) {
$user->get('guid')
->shouldBeCalled()
->willReturn(1000);
$user->get('name')
->shouldBeCalled()
->willReturn('PHPSpec');
$this->repository->getList([
'user_guid' => 1000
])
->shouldBeCalled()
->willReturn($getListResponse);
$getListResponse->first()
->shouldBeCalled()
->willReturn($settings);
$settings->getTitle()
->shouldBeCalled()
->willReturn('');
$settings->setTitle('PHPSpec')
->shouldBeCalled()
->willReturn($settings);
$this->setupRoutingDelegate->onUpdate($settings)
->shouldBeCalled();
$this->repository->add($settings)
->shouldBeCalled()
->willReturn(true);
$this
->shouldNotThrow(Exception::class)
->duringOnEnable($user);
}
}
<?php
namespace Spec\Minds\Core\Pro\Delegates;
use Exception;
use Minds\Core\Config;
use Minds\Core\Pro\Delegates\SetupRoutingDelegate;
use Minds\Core\Pro\Domain\EdgeRouters\EdgeRouterInterface;
use Minds\Core\Pro\Settings;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class SetupRoutingDelegateSpec extends ObjectBehavior
{
/** @var Config */
protected $config;
/** @var EdgeRouterInterface */
protected $edgeRouter;
public function let(
Config $config,
EdgeRouterInterface $edgeRouter
) {
$this->config = $config;
$this->edgeRouter = $edgeRouter;
$this->beConstructedWith($config, $edgeRouter);
}
public function it_is_initializable()
{
$this->shouldHaveType(SetupRoutingDelegate::class);
}
public function it_should_setup_routing_on_update_with_default_subdomain(
Settings $settings
) {
$settings->getUserGuid()
->shouldBeCalled()
->willReturn(1000);
$settings->getDomain()
->shouldBeCalled()
->willReturn(null);
$this->config->get('pro')
->shouldBeCalled()
->willReturn([
'subdomain_suffix' => 'phpspec.test',
]);
$settings->setDomain('pro-1000.phpspec.test')
->shouldBeCalled()
->willReturn($settings);
$this->edgeRouter->initialize()
->shouldBeCalled()
->willReturn($this->edgeRouter);
$this->edgeRouter->putEndpoint($settings)
->shouldBeCalled()
->willReturn(true);
$this
->shouldNotThrow(Exception::class)
->duringOnUpdate($settings);
}
public function it_should_setup_routing_on_update_with_a_custom_domain(
Settings $settings
) {
$settings->getUserGuid()
->shouldBeCalled()
->willReturn(1000);
$settings->getDomain()
->shouldBeCalled()
->willReturn('routing-test.phpspec.test');
$settings->setDomain(Argument::cetera())
->shouldNotBeCalled();
$this->edgeRouter->initialize()
->shouldBeCalled()
->willReturn($this->edgeRouter);
$this->edgeRouter->putEndpoint($settings)
->shouldBeCalled()
->willReturn(true);
$this
->shouldNotThrow(Exception::class)
->duringOnUpdate($settings);
}
}
<?php
namespace Spec\Minds\Core\Pro\Domain\EdgeRouters;
use Aws\DynamoDb\DynamoDbClient;
use Minds\Core\Config;
use Minds\Core\Pro\Domain\EdgeRouters\TraefikDynamoDb;
use Minds\Core\Pro\Settings;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class TraefikDynamoDbSpec extends ObjectBehavior
{
/** @var Config */
protected $config;
/** @var DynamoDbClient */
protected $dynamoDb;
public function let(
Config $config,
DynamoDbClient $dynamoDbClient
) {
$this->config = $config;
$this->dynamoDb = $dynamoDbClient;
$this->beConstructedWith($config, $dynamoDbClient);
}
public function it_is_initializable()
{
$this->shouldHaveType(TraefikDynamoDb::class);
}
// NOTE: Cannot mock $this->initialize()
public function it_should_put_endpoint(
Settings $settings
) {
$settings->getDomain()
->shouldBeCalled()
->willReturn('phpspec.test');
$settings->getUserGuid()
->shouldBeCalled()
->willReturn(1000);
$this->config->get('pro')
->shouldBeCalled()
->willReturn([
'dynamodb_table_name' => 'phpspec'
]);
$this->dynamoDb->putItem(Argument::that(function ($args) {
return $args['TableName'] === 'phpspec';
}))
->shouldBeCalled()
->willReturn(true);
$this
->putEndpoint($settings)
->shouldReturn(true);
}
}
<?php
namespace Spec\Minds\Core\Pro\Domain;
use Minds\Common\Cookie;
use Minds\Common\Jwt;
use Minds\Core\Config;
use Minds\Core\Pro\Domain\Security;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class SecuritySpec extends ObjectBehavior
{
/** @var Cookie */
protected $cookie;
/** @var Jwt */
protected $jwt;
/** @var Config */
protected $config;
public function let(
Cookie $cookie,
Jwt $jwt,
Config $config
) {
$this->cookie = $cookie;
$this->jwt = $jwt;
$this->config = $config;
$this->beConstructedWith($cookie, $jwt, $config);
}
public function it_is_initializable()
{
$this->shouldHaveType(Security::class);
}
public function it_should_set_up()
{
$this->jwt->randomString()
->shouldBeCalled()
->willReturn('~random~');
$this->config->get('oauth')
->shouldBeCalled()
->willReturn([
'encryption_key' => 'phpspec'
]);
$this->jwt->setKey('phpspec')
->shouldBeCalled()
->willReturn($this->jwt);
$this->jwt->encode(Argument::type('array'), Argument::type('int'), Argument::type('int'))
->shouldBeCalled()
->willReturn('~encoded~');
$this->cookie->setName(Security::JWT_COOKIE_NAME)
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setName(Security::XSRF_COOKIE_NAME)
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setValue('~encoded~')
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setValue('~random~')
->shouldBeCalled()
->willReturn($this->cookie);
$this->cookie->setExpire(Argument::type('int'))
->shouldBeCalledTimes(2)
->willReturn($this->cookie);
$this->cookie->setPath('/')
->shouldBeCalledTimes(2)
->willReturn($this->cookie);
$this->cookie->setHttpOnly(false)
->shouldBeCalledTimes(2)
->willReturn($this->cookie);
$this->cookie->create()
->shouldBeCalledTimes(2)
->willReturn(true);
$this
->setUp('phpspec.test')
->shouldReturn('~encoded~');
}
}
<?php
namespace Spec\Minds\Core\Pro;
use Minds\Common\Repository\Response;
use Minds\Core\Config;
use Minds\Core\Pro\Domain;
use Minds\Core\Pro\Repository;
use Minds\Core\Pro\Settings;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class DomainSpec extends ObjectBehavior
{
/** @var Config */
protected $config;
/** @var Repository */
protected $repository;
public function let(
Config $config,
Repository $repository
) {
$this->config = $config;
$this->repository = $repository;
$this->beConstructedWith($config, $repository);
}
public function it_is_initializable()
{
$this->shouldHaveType(Domain::class);
}
public function it_should_lookup(
Response $getListResponse,
Settings $settings
) {
$this->config->get('pro')
->shouldBeCalled()
->willReturn([
'root_domains' => ['phpspec.test']
]);
$this->repository->getList([
'domain' => 'phpspec-test.com'
])
->shouldBeCalled()
->willReturn($getListResponse);
$getListResponse->first()
->shouldBeCalled()
->willReturn($settings);
$this
->lookup('phpspec-test.com')
->shouldReturn($settings);
}
public function it_should_not_lookup_if_root_domain()
{
$this->config->get('pro')
->shouldBeCalled()
->willReturn([
'root_domains' => ['phpspec.test']
]);
$this->repository->getList(Argument::cetera())
->shouldNotBeCalled();
$this
->lookup('phpspec.test')
->shouldReturn(null);
}
public function it_should_get_icon(
Settings $settings,
User $owner
) {
$owner->getIconURL(Argument::type('string'))
->shouldBeCalled()
->willReturn('http://phpspec/icon');
$this
->getIcon($settings, $owner)
->shouldReturn('http://phpspec/icon');
}
}
<?php
namespace Spec\Minds\Core\Pro;
use Minds\Core\Pro\Settings;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class SettingsSpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(Settings::class);
}
public function it_should_get_one_line_headline_from_single_line_value()
{
$this->setHeadline('This is a headline');
$this
->getOneLineHeadline()
->shouldReturn('This is a headline');
}
public function it_should_get_one_line_headline_from_multi_line_value()
{
$this->setHeadline("This is a headline.\nOther line");
$this
->getOneLineHeadline()
->shouldReturn('This is a headline. Other line');
}
public function it_should_export()
{
$this
->export()
->shouldBeArray();
}
public function it_should_build_styles()
{
$this
->buildStyles()
->shouldBeArray();
}
public function it_should_calc_tile_ratio_percentage()
{
$this
->setTileRatio('1:1');
$this
->calcTileRatioPercentage()
->shouldReturn(100.0);
$this
->setTileRatio('4:3');
$this
->calcTileRatioPercentage()
->shouldReturn(75.0);
$this
->setTileRatio('16:10');
$this
->calcTileRatioPercentage()
->shouldReturn(62.5);
$this
->setTileRatio('16:9');
$this
->calcTileRatioPercentage()
->shouldReturn(56.25);
$this
->setTileRatio('');
$this
->calcTileRatioPercentage()
->shouldReturn(56.25);
}
}
<?php
namespace Spec\Minds\Core\Referrals;
use Minds\Core\Referrals\ReferralCookie;
use Minds\Entities\User;
use Minds\Entities\Activity;
use Zend\Diactoros\ServerRequest;
use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Uri;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ReferralCookieSpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(ReferralCookie::class);
}
public function it_should_set_a_referral_cookie_from_a_referral_param()
{
$request = (new ServerRequest())->withQueryParams(['referrer' => 'mark']);
$this->withRouterRequest($request);
$this->create();
expect($_COOKIE['referrer'])
->toBe('mark');
}
public function it_should_not_set_cookie_if_already_present()
{
$_COOKIE['referrer'] = 'bill';
$request = (new ServerRequest())
->withCookieParams(['referrer' => 'bill'])
->withQueryParams(['referrer' => 'mark']);
$this->withRouterRequest($request);
$this->create();
expect($_COOKIE['referrer'])
->toBe('bill');
}
public function it_should_set_cookie_from_user_entity()
{
$user = new User();
$user->guid = 123;
$request = (new ServerRequest());
$this->withRouterRequest($request);
$this->setEntity($user);
$this->create();
expect($_COOKIE['referrer'])
->toBe(123);
}
public function it_should_set_cookie_from_activity()
{
$activity = new Activity();
$activity->guid = 123;
$activity->owner_guid = 456;
$request = (new ServerRequest());
$this->withRouterRequest($request);
$this->setEntity($activity);
$this->create();
expect($_COOKIE['referrer'])
->toBe(456);
}
public function it_should_not_allow_entity_to_override_param()
{
$activity = new Activity();
$activity->guid = 123;
$activity->owner_guid = 456;
$request = (new ServerRequest())
->withQueryParams(['referrer' => 'mark']);
;
$this->withRouterRequest($request);
$this->setEntity($activity);
$this->create();
expect($_COOKIE['referrer'])
->toBe('mark');
}
public function it_should_not_allow_entity_to_override_cookie()
{
$activity = new Activity();
$activity->guid = 123;
$activity->owner_guid = 456;
$request = (new ServerRequest())
->withCookieParams(['referrer' => 'mark']);
;
$this->withRouterRequest($request);
$this->setEntity($activity);
$this->create();
expect($_COOKIE['referrer'])
->toBe('mark');
}
}
......@@ -476,7 +476,8 @@ $CONFIG->set('features', [
'cassandra-notifications' => true,
'dark-mode' => true,
'allow-comments-toggle' => false,
'permissions' => false
'permissions' => false,
'pro' => false,
]);
$CONFIG->set('email', [
......@@ -572,3 +573,9 @@ $CONFIG->set('gitlab', [
],
'private_key' => ''
]);
$CONFIG->set('pro', [
'root_domains' => ['minds.com', 'www.minds.com', 'localhost'],
'subdomain_suffix' => 'minds.com',
'dynamodb_table_name' => 'traefik',
]);