...
 
Commits (139)
......@@ -26,3 +26,5 @@ node_modules
!/.travis.yml
coverage
!/.gitlab
composer.phar
settings.php-*.bak
......@@ -16,7 +16,7 @@ build:
- apk add --no-cache git
- sh tools/setup.sh production
artifacts:
name: '$CI_COMMIT_REF_SLUG'
name: "$CI_COMMIT_REF_SLUG"
paths:
- vendor
- bin
......@@ -39,7 +39,7 @@ prepare:fpm:
services:
- docker:dind
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- |
docker build \
-t $CI_REGISTRY_IMAGE/fpm:$CI_PIPELINE_ID \
......@@ -80,29 +80,30 @@ review:start:
image: minds/helm-eks:latest
script:
- aws eks update-kubeconfig --name=sandbox
- git clone --branch=sandbox-wip https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/minds/helm-charts.git
- git clone --branch=master https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/minds/helm-charts.git
- echo "Upgrading helm for pipeline ${CI_PIPELINE_ID}"
- echo "Setting to image ${CI_REGISTRY_IMAGE}"
- "helm upgrade \
--install \
--reuse-values \
--set phpfpm.image.repository=$CI_REGISTRY_IMAGE/fpm \
--set-string phpfpm.image.tag=$CI_PIPELINE_ID \
--set runners.image.repository=$CI_REGISTRY_IMAGE/runners \
--set-string runners.image.tag=$CI_PIPELINE_ID \
--set domain=$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN \
--set elasticsearch.clusterName=$CI_BUILD_REF_SLUG-elasticsearch \
--wait \
$CI_BUILD_REF_SLUG \
./helm-charts/minds"
--install \
--reuse-values \
--set phpfpm.image.repository=$CI_REGISTRY_IMAGE/fpm \
--set-string phpfpm.image.tag=$CI_PIPELINE_ID \
--set runners.image.repository=$CI_REGISTRY_IMAGE/runners \
--set-string runners.image.tag=$CI_PIPELINE_ID \
--set domain=$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN \
--set elasticsearch.clusterName=$CI_BUILD_REF_SLUG-elasticsearch \
--wait \
$CI_BUILD_REF_SLUG \
./helm-charts/minds"
- sentry-cli releases deploys $CI_PIPELINE_ID new -e review-$CI_COMMIT_REF_SLUG
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN
on_stop: review:stop
except:
except:
refs:
- master
- production
- test/gitlab-ci
review:stop:
......@@ -118,9 +119,10 @@ review:stop:
variables:
GIT_STRATEGY: none
when: manual
except:
except:
refs:
- master
- production
- test/gitlab-ci
qa:manual:
......@@ -131,16 +133,20 @@ qa:manual:
only:
refs:
- master
- production
- test/gitlab-ci
allow_failure: false
staging:fpm:
################
# Deploy Stage #
################
.deploy: &deploy
stage: deploy:staging
image: minds/ci:latest
services:
- docker:dind
script:
- IMAGE_LABEL="staging"
- $(aws ecr get-login --no-include-email --region us-east-1)
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker pull $CI_REGISTRY_IMAGE/fpm:$CI_PIPELINE_ID
......@@ -151,40 +157,51 @@ staging:fpm:
- docker tag $CI_REGISTRY_IMAGE/fpm:$CI_PIPELINE_ID $CI_REGISTRY_IMAGE/fpm:$IMAGE_LABEL
- docker push $CI_REGISTRY_IMAGE/fpm:$IMAGE_LABEL
# Deploy to staging
- aws ecs update-service --service=$ECS_APP_STAGING_SERVICE --force-new-deployment --region us-east-1 --cluster=$ECS_CLUSTER
- aws ecs update-service --service=$ECS_SERVICE --force-new-deployment --region us-east-1 --cluster=$ECS_CLUSTER
# Update sentry
- sentry-cli releases deploys $CI_PIPELINE_ID new -e $IMAGE_LABEL
staging:fpm:
<<: *deploy
stage: deploy:staging
variables:
IMAGE_LABEL: "staging"
ECS_SERVICE: $ECS_APP_STAGING_SERVICE
environment:
name: staging
url: https://www.minds.com/?staging=1
only:
only:
refs:
- master
- production
- test/gitlab-ci
review:preprod:
<<: *deploy
stage: review
when: manual
allow_failure: true
variables:
IMAGE_LABEL: "preprod"
ECS_SERVICE: $ECS_APP_PREPROD_SERVICE
environment:
name: preprod
url: https://www.minds.com/?preprod=1
# except:
# refs:
# - master
# - test/gitlab-ci
canary:fpm:
<<: *deploy
stage: deploy:canary
image: minds/ci:latest
services:
- docker:dind
script:
- IMAGE_LABEL="canary"
- $(aws ecr get-login --no-include-email --region us-east-1)
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker pull $CI_REGISTRY_IMAGE/fpm:$CI_PIPELINE_ID
# Push to AWS registry
- docker tag $CI_REGISTRY_IMAGE/fpm:$CI_PIPELINE_ID $ECR_REPOSITORY_URL_FPM:$IMAGE_LABEL
- docker push $ECR_REPOSITORY_URL_FPM:$IMAGE_LABEL
# Push to Gitlab registry
- docker tag $CI_REGISTRY_IMAGE/fpm:$CI_PIPELINE_ID $CI_REGISTRY_IMAGE/fpm:$IMAGE_LABEL
- docker push $CI_REGISTRY_IMAGE/fpm:$IMAGE_LABEL
# Deploy to ECS
- aws ecs update-service --service=$ECS_APP_CANARY_SERVICE --force-new-deployment --region us-east-1 --cluster=$ECS_CLUSTER
# Update sentry
- sentry-cli releases deploys $CI_PIPELINE_ID new -e $IMAGE_LABEL
variables:
IMAGE_LABEL: "canary"
ECS_SERVICE: $ECS_APP_CANARY_SERVICE
only:
refs:
- master
- production
- test/gitlab-ci
environment:
name: canary
......@@ -193,28 +210,15 @@ canary:fpm:
allow_failure: false # prevents auto deploy to full production
production:fpm:
<<: *deploy
stage: deploy:production
image: minds/ci:latest
services:
- docker:dind
script:
- IMAGE_LABEL="production"
- $(aws ecr get-login --no-include-email --region us-east-1)
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker pull $CI_REGISTRY_IMAGE/fpm:$CI_PIPELINE_ID
# Push to AWS registry
- docker tag $CI_REGISTRY_IMAGE/fpm:$CI_PIPELINE_ID $ECR_REPOSITORY_URL_FPM:$IMAGE_LABEL
- docker push $ECR_REPOSITORY_URL_FPM:$IMAGE_LABEL
# Push to Gitlab registry
- docker tag $CI_REGISTRY_IMAGE/fpm:$CI_PIPELINE_ID $CI_REGISTRY_IMAGE/fpm:$IMAGE_LABEL
- docker push $CI_REGISTRY_IMAGE/fpm:$IMAGE_LABEL
# Delpoy to ECS
- aws ecs update-service --service=$ECS_APP_PRODUCTION_SERVICE --force-new-deployment --region us-east-1 --cluster=$ECS_CLUSTER
# Update sentry
- sentry-cli releases deploys $CI_PIPELINE_ID new -e $IMAGE_LABEL
variables:
IMAGE_LABEL: "production"
ECS_SERVICE: $ECS_APP_PRODUCTION_SERVICE
only:
refs:
- master
- production
- test/gitlab-ci
environment:
name: production
......@@ -242,6 +246,7 @@ production:runners:
only:
refs:
- master
- production
- test/gitlab-ci
environment:
name: production
......
......@@ -2,10 +2,7 @@
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;
use Minds\Core\Session;
......@@ -111,11 +108,17 @@ class Factory
static::setCORSHeader();
$code = !Security\XSRF::validateRequest() ? 403 : 401;
if (isset($_SERVER['HTTP_APP_VERSION'])) {
$code = 401; // Mobile requires 401 errors
}
header('Content-type: application/json');
header('HTTP/1.1 401 Unauthorized', true, 401);
http_response_code($code);
echo json_encode([
'error' => 'Sorry, you are not authenticated',
'code' => 401,
'code' => $code,
'loggedin' => false
]);
exit;
......@@ -178,7 +181,10 @@ class Factory
'status' => 'success', //should success be assumed?
], $data);
ob_end_clean();
if (ob_get_level() > 1) {
// New PSR-7 Router has an OB started all the time
ob_end_clean();
}
static::setCORSHeader();
......
File added
......@@ -259,6 +259,16 @@ class Response implements \Iterator, \ArrayAccess, \Countable, \JsonSerializable
return count($this->data);
}
/**
* @param array $data
* @return Response
*/
public function pushArray(array $data)
{
array_push($this->data, ...$data);
return $this;
}
/**
* Exports the data array
* @return array
......@@ -329,6 +339,18 @@ class Response implements \Iterator, \ArrayAccess, \Countable, \JsonSerializable
return array_reduce($this->data, $callback, $initialValue);
}
/**
* @param callable $callback
* @return Response
*/
public function sort(callable $callback): Response
{
$data = $this->data;
usort($data, $callback);
return new static($data, $this->pagingToken);
}
/**
* Returns the first element of the Response, or null if empty
* @return mixed|null
......
......@@ -2,13 +2,22 @@
/**
* Converts a static class to use instances
*/
namespace Minds\Common;
use ReflectionClass;
use ReflectionException;
class StaticToInstance
{
/** @var $class */
/** @var ReflectionClass */
private $class;
/**
* StaticToInstance constructor.
* @param $class
* @throws ReflectionException
*/
public function __construct($class)
{
$this->setClass($class);
......@@ -16,11 +25,13 @@ class StaticToInstance
/**
* Set the class in question
* @return StripeStaticToOO
* @param $class
* @return static
* @throws ReflectionException
*/
public function setClass($class)
{
$this->class = new \ReflectionClass($class);
$this->class = new ReflectionClass($class);
return clone $this;
}
......@@ -28,7 +39,7 @@ class StaticToInstance
* Call the static functions as OO style
* @param string $method
* @param array $arguments
* @return midex
* @return mixed
*/
public function __call($method, $arguments)
{
......
......@@ -3,7 +3,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title></title>
<style>
p {
p, li {
font-family: Roboto,Arial,sans-serif;
font-size: 18px;
line-height: 1.5;
......
......@@ -9,6 +9,7 @@ use Minds\Entities\User;
use Minds\Core\Email\Campaigns\UserRetention\GoneCold;
use Minds\Core\Email\Campaigns\WhenBoost;
use Minds\Core\Email\Campaigns\WireReceived;
use Minds\Core\Email\Campaigns\WirePromotions;
use Minds\Core\Email\Campaigns\UserRetention\WelcomeComplete;
use Minds\Core\Email\Campaigns\UserRetention\WelcomeIncomplete;
use Minds\Core\Suggestions\Manager;
......@@ -189,6 +190,28 @@ class Email extends Cli\Controller implements Interfaces\CliControllerInterface
}
}
public function testWirePromotion()
{
$userguid = $this->getOpt('guid');
$output = $this->getOpt('output');
$send = $this->getOpt('send');
$user = new User($userguid);
if (!$user->guid) {
$this->out('User not found');
exit;
}
$campaign = (new WirePromotions())
->setUser($user);
$message = $campaign->build();
if ($send) {
$campaign->send();
}
}
public function testWelcomeIncomplete()
{
$userguid = $this->getOpt('guid');
......
......@@ -37,7 +37,7 @@ class EntityCentric extends Cli\Controller implements Interfaces\CliControllerIn
$i = 0;
foreach ($manager->sync() as $record) {
$this->out(++$i);
$this->out(++$i .": {$record->getUrn()}");
}
}
}
<?php
/**
* Features
*
* @author edgebal
*/
namespace Minds\Controllers\Cli;
use Minds\Cli;
use Minds\Core\Di\Di;
use Minds\Core\Features\Manager;
use Minds\Exceptions\CliException;
use Minds\Interfaces;
class Features extends Cli\Controller implements Interfaces\CliControllerInterface
{
/**
* @inheritDoc
*/
public function help($command = null)
{
$this->out('Syntax usage: cli features sync');
}
/**
* @inheritDoc
*/
public function exec()
{
return $this->help();
}
public function sync()
{
/** @var Manager $manager */
$manager = Di::_()->get('Features\Manager');
$ttl = $this->getOpt('ttl') ?: 300;
$environmentList = array_filter(explode(',', $this->getOpt('environment') ?: ''));
if (!$environmentList) {
throw new CliException('Specify an environment');
}
while (true /* Forever running task */) {
foreach ($environmentList as $environment) {
$this->out([
date('c'),
"TTL: {$ttl}",
"Environment: {$environment}"
], static::OUTPUT_PRE);
$sync = $manager
->setEnvironment($environment)
->sync($ttl);
foreach ($sync as $key => $output) {
$this->out(sprintf("Sync %s: %s", $key, $output));
}
}
if (!$this->getOpt('forever')) {
break;
}
$this->out("Done, sleeping {$ttl}s");
sleep($ttl);
}
}
}
......@@ -64,7 +64,8 @@ class Install extends Cli\Controller implements Interfaces\CliControllerInterfac
if ($installType == "all" || $installType == "cassandra") {
$this->out('- Provisioning Cassandra:', $this::OUTPUT_INLINE);
$isCleanCassandra = $this->getopt("cleanCassandra") != null;
$provisioner->provisionCassandra(null, $isCleanCassandra);
$exitOnFailure = (bool) $this->getopt("exitOnFailure");
$provisioner->provisionCassandra(null, $isCleanCassandra, $exitOnFailure);
$this->out('OK');
$this->out('- Emptying Cassandra pool:', $this::OUTPUT_INLINE);
......@@ -85,7 +86,7 @@ class Install extends Cli\Controller implements Interfaces\CliControllerInterfac
$provisioner->setupFirstAdmin();
$this->out('OK');
} catch (\Exception $ex) {
$this->out('Could not setup initial user');
$this->out("Could not setup initial user: {$ex->getMessage()}");
}
}
......
......@@ -67,4 +67,55 @@ class Stripe extends Cli\Controller implements Interfaces\CliControllerInterface
$intent = $intentManager->get($id);
var_dump($intent);
}
public function fix_connect()
{
$connectManager = new Core\Payments\Stripe\Connect\Manager();
$i = 0;
foreach ($connectManager->getList() as $account) {
++$i;
echo "\n$i $account->id";
var_dump($account->requirements->currently_due);
}
}
public function remove_business_type()
{
$connectManager = new Core\Payments\Stripe\Connect\Manager();
$account = $connectManager->getByAccountId($this->getOpt('id'));
$connectManager->update($account);
}
public function create_stripe_lookups()
{
$connectManager = new Core\Payments\Stripe\Connect\Manager();
$iterator = new Core\Analytics\Iterators\SignupsOffsetIterator();
$iterator->token = $this->getOpt('token');
$i = 0;
$s = 0;
foreach ($iterator as $user) {
if (!$user instanceof Entities\User) {
continue;
}
++$i;
var_dump($user->getMerchant());
if ($stripeId = $user->getMerchant()['id']) {
++$s;
}
echo "\n$s/$i $user->guid {$stripeId} ($iterator->token)";
if (!$stripeId) {
continue;
}
try {
$account = $connectManager->getByAccountId($stripeId);
$account->setEmail($user->getEmail());
$account->setUrl('https://www.minds.com/' . $user->username);
$account->setMetadata([
'guid' => (string) $user->guid,
]);
$connectManager->update($account);
} catch (\Exception $e) {
}
}
}
}
......@@ -4,7 +4,7 @@ namespace Minds\Controllers\Cli;
use Minds\Core\Minds;
use Minds\Cli;
use Minds\Core\Feeds\Top\Manager;
use Minds\Core\Feeds\Elastic\Manager;
use Minds\Exceptions\CliException;
use Minds\Interfaces;
......
<?php
namespace Minds\Controllers\Cli\Top;
use Exception;
use Minds\Core\Feeds\Elastic\Sync;
use Minds\Core\Minds;
use Minds\Cli;
use Minds\Exceptions\CliException;
use Minds\Interfaces;
class All extends Cli\Controller implements Interfaces\CliControllerInterface
{
/** @var Sync */
private $sync;
/**
* Top constructor.
*/
public function __construct()
{
$minds = new Minds();
$minds->start();
$this->sync = new Sync();
}
/**
* @param null $command
* @return void
*/
public function help($command = null)
{
$this->out('Syntax usage: cli top all sync_<type> --metric=? --from=? --to=?');
}
/**
* @return void
*/
public function exec()
{
$this->help();
}
/**
* @throws CliException
*/
public function sync_activity(): void
{
list($from, $to) = $this->getTimeRangeFromArgs();
$this->syncBy('activity', null, $this->getOpt('metric'), $from, $to);
}
/**
* @throws CliException
*/
public function sync_images(): void
{
list($from, $to) = $this->getTimeRangeFromArgs();
$this->syncBy('object', 'image', $this->getOpt('metric'), $from, $to);
}
/**
* @throws CliException
*/
public function sync_videos(): void
{
list($from, $to) = $this->getTimeRangeFromArgs();
$this->syncBy('object', 'video', $this->getOpt('metric'), $from, $to);
}
/**
* @throws CliException
*/
public function sync_blogs(): void
{
list($from, $to) = $this->getTimeRangeFromArgs();
$this->syncBy('object', 'blog', $this->getOpt('metric'), $from, $to);
}
/**
* @throws CliException
*/
public function sync_groups(): void
{
list($from, $to) = $this->getTimeRangeFromArgs();
$this->syncBy('group', null, $this->getOpt('metric'), $from, $to);
}
/**
* @throws CliException
*/
public function sync_channels(): void
{
list($from, $to) = $this->getTimeRangeFromArgs();
$this->syncBy('user', null, $this->getOpt('metric'), $from, $to);
}
/**
* @return int[]
* @throws CliException
*/
protected function getTimeRangeFromArgs(): array
{
$to = $this->getOpt('to') ?: time();
if ($this->getOpt('from') && $this->getOpt('secsAgo')) {
throw new CliException('Cannot specify both `from` and `secsAgo`');
} elseif (!$this->getOpt('from') && !$this->getOpt('secsAgo')) {
throw new CliException('You should specify either `from` or `secsAgo`');
}
if ($this->getOpt('secsAgo')) {
$from = time() - $this->getOpt('secsAgo');
} else {
$from = $this->getOpt('from');
}
return [$from, $to];
}
/**
* @param $type
* @param $subtype
* @param $metric
* @param $from
* @param $to
* @throws CliException
* @throws Exception
*/
protected function syncBy($type, $subtype, $metric, $from, $to): void
{
if (!$metric) {
throw new CliException('Missing `metric`');
}
if (!$from || !is_numeric($from)) {
throw new CliException('Missing or invalid `from` value');
}
if (!$to || !is_numeric($to)) {
throw new CliException('Invalid `to` value');
}
if ($from > $to) {
throw new CliException('`from` must be lesser than `to`');
}
error_reporting(E_ALL);
ini_set('display_errors', 1);
$displayType = trim(implode(':', [$type, $subtype]), ':');
$this->out(sprintf(
"%s -> %s",
date('r', $from),
date('r', $to)
));
$this->out("Syncing {$displayType} / {$metric}");
$this->sync
->setType($type ?: '')
->setSubtype($subtype ?: '')
->setMetric($metric)
->setFrom($from * 1000)
->setTo($to * 1000)
->run();
$this->out("\nCompleted syncing '{$displayType}'.");
}
}
......@@ -21,9 +21,41 @@ class Transcode extends Cli\Controller implements Interfaces\CliControllerInterf
public function exec()
{
$transcoder = new Core\Media\Services\FFMpeg;
$transcoder->setKey($this->getOpt('guid'));
$transcoder->setFullHD($this->getOpt('full_hd') ?? false);
$transcoder->onQueue();
$entity = Di::_()->get('EntitiesBuilder')->single($this->getOpt('guid'));
if (!$entity) {
$this->out('Entity not found');
return;
}
$manager = Di::_()->get('Media\Video\Transcoder\Manager');
$manager->createTranscodes($entity);
}
/**
* Retries the transcode on the current thread
* @return void
*/
public function retry()
{
$entity = Di::_()->get('EntitiesBuilder')->single($this->getOpt('guid'));
if (!$entity) {
$this->out('Entity not found');
return;
}
$manager = Di::_()->get('Media\Video\Transcoder\Manager');
$transcode = $manager->getList([
'guid' => $this->getOpt('guid'),
'profileId' => $this->getOpt('profile-id'),
])[0];
if (!$transcode) {
$this->out('Transcode not found');
return;
}
$manager->transcode($transcode);
}
}
......@@ -81,4 +81,21 @@ class User extends Cli\Controller implements Interfaces\CliControllerInterface
$this->out($e);
}
}
public function register_complete()
{
$username = $this->getOpt('username');
if (!$username) {
throw new Exceptions\CliException('Missing username');
}
$user = new Entities\User(strtolower($username));
if (!$user->guid) {
throw new Exceptions\CliException('User does not exist');
}
Core\Events\Dispatcher::trigger('register/complete', 'user', [ 'user' => $user ]);
}
}
......@@ -104,8 +104,8 @@ class authenticate implements Interfaces\Api, Interfaces\ApiIgnorePam
Security\XSRF::setCookie(true);
// Set the canary cookie
Di::_()->get('Features\Manager')
->setCanaryCookie($user->isCanary());
Di::_()->get('Features\Canary')
->setCookie($user->isCanary());
$response['status'] = 'success';
$response['user'] = $user->export();
......
......@@ -220,6 +220,11 @@ class blog implements Interfaces\Api
$blog->setMature(!!$_POST['mature']);
}
if (isset($_POST['nsfw'])) {
$nsfw = !is_array($_POST['nsfw']) ? json_decode($_POST['nsfw']) : $_POST['nsfw'];
$blog->setNsfw($nsfw);
}
if (isset($_POST['wire_threshold'])) {
$threshold = is_string($_POST['wire_threshold']) ? json_decode($_POST['wire_threshold']) : $_POST['wire_threshold'];
$blog->setWireThreshold($threshold);
......@@ -266,7 +271,7 @@ class blog implements Interfaces\Api
}
if ($blog->isMonetized()) {
if ($blog->isMature()) {
if ($blog->getNsfw() || $blog->isMature()) {
return Factory::response([
'status' => 'error',
'message' => 'Cannot monetize an explicit blog'
......@@ -283,7 +288,8 @@ class blog implements Interfaces\Api
}
}
if (isset($_POST['mature']) && $_POST['mature']) {
if ((isset($_POST['nsfw']) && $_POST['nsfw'])
|| (isset($_POST['mature']) && $_POST['mature'])) {
$user = Core\Session::getLoggedInUser();
if (!$user->getMatureContent()) {
......@@ -331,6 +337,15 @@ class blog implements Interfaces\Api
]);
}
// This is a first create blog that should have a banner
// We are trying to stop spam with this check
if ($blog->isPublished() && !$editing && !is_uploaded_file($_FILES['file']['tmp_name'])) {
return Factory::response([
'status' => 'error',
'message' => 'You must upload a banner'
]);
}
try {
if ($editing) {
$saved = $manager->update($blog);
......
......@@ -46,6 +46,8 @@ class groups implements Interfaces\Api
$response['load-next'] = (string) key($guids);
break;
case "member":
Factory::isLoggedIn();
$manager = new Membership();
$guids = $manager->getGroupsByMember([
'user_guid' => $user->guid,
......
......@@ -263,10 +263,10 @@ class media implements Interfaces\Api, Interfaces\ApiIgnorePam
$entity = Core\Media\Factory::build($clientType);
$container_guid = isset($data['container_guid']) && is_numeric($data['container_guid']) ? $data['container_guid'] : null;
$entity->patch([
'title' => isset($data['name']) ? $data['name'] : '',
'mature' => isset($data['mature']) && !!$data['mature'],
'nsfw' => !is_array($_POST['nsfw']) ? json_decode($_POST['nsfw']) : $_POST['nsfw'],
'batch_guid' => 0,
'access_id' => 0,
'owner_guid' => $user->guid,
......
......@@ -539,6 +539,7 @@ class newsfeed implements Interfaces\Api
$activity = new Activity();
$activity->setMature(isset($_POST['mature']) && !!$_POST['mature']);
$activity->setNsfw($_POST['nsfw'] ?? []);
$user = Core\Session::getLoggedInUser();
......
......@@ -45,9 +45,9 @@ class register implements Interfaces\Api, Interfaces\ApiIgnorePam
}
try {
$captcha = Core\Di\Di::_()->get('Security\ReCaptcha');
$captcha->setAnswer($_POST['captcha']);
if (isset($_POST['captcha']) && !$captcha->validate()) {
$captcha = Core\Di\Di::_()->get('Captcha\Manager');
if (isset($_POST['captcha']) && !$captcha->verifyFromClientJson($_POST['captcha'])) {
throw new \Exception('Captcha failed');
}
......@@ -78,7 +78,7 @@ class register implements Interfaces\Api, Interfaces\ApiIgnorePam
$hasSignupTags = false;
if (isset($_COOKIE['mexp'])) {
$manager = Core\Di\Di::_()->get('Experiments\Manager');
$bucket = $manager->getBucketForExperiment('Homepage200619');
$bucket = $manager->getBucketForExperiment('Homepage121119');
$user->expHomepage200619 = $bucket->getId();
}
......
......@@ -11,6 +11,7 @@ use Minds\Api\Factory;
use Minds\Core;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Email\Confirmation\Manager as EmailConfirmation;
use Minds\Core\Queue\Client as Queue;
use Minds\Entities;
use Minds\Interfaces;
......@@ -79,8 +80,14 @@ class settings implements Interfaces\Api
$user->name = trim($_POST['name']);
}
$emailChange = false;
if (isset($_POST['email']) && $_POST['email']) {
$user->setEmail($_POST['email']);
if (strtolower($_POST['email']) !== strtolower($user->getEmail())) {
$emailChange = true;
}
}
if (isset($_POST['boost_rating'])) {
......@@ -146,6 +153,23 @@ class settings implements Interfaces\Api
$response['status'] = 'error';
}
if ($emailChange) {
/** @var EmailConfirmation $emailConfirmation */
$emailConfirmation = Di::_()->get('Email\Confirmation');
$emailConfirmation
->setUser($user);
$reset = $emailConfirmation
->reset();
if ($reset) {
$emailConfirmation
->sendEmail();
} else {
error_log('Cannot reset email confirmation for ' . $user->guid);
}
}
return Factory::response($response);
}
......
<?php
/**
* features
*
* @author edgebal
*/
namespace Minds\Controllers\api\v2\admin;
use Exception;
use Minds\Api\Factory;
use Minds\Core\Features\Manager;
use Minds\Core\Session;
use Minds\Entities\User;
use Minds\Interfaces;
use Minds\Core\Di\Di;
class features implements Interfaces\Api, Interfaces\ApiAdminPam
{
/**
* @inheritDoc
*/
public function get($pages)
{
$for = null;
if (isset($_GET['for'])) {
try {
$for = new User(strtolower($_GET['for']));
if (!$for || !$for->guid) {
$for = null;
}
} catch (Exception $e) {
$for = null;
}
}
/** @var Manager $manager */
$manager = Di::_()->get('Features\Manager');
return Factory::response(
$manager->breakdown($for)
);
}
/**
* @inheritDoc
*/
public function post($pages)
{
return Factory::response([]);
}
/**
* @inheritDoc
*/
public function put($pages)
{
return Factory::response([]);
}
/**
* @inheritDoc
*/
public function delete($pages)
{
return Factory::response([]);
}
}
......@@ -112,8 +112,8 @@ class firehose implements Interfaces\Api, Interfaces\ApiAdminPam
}
if ($type !== 'activity') {
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
/** @var Core\Feeds\Elastic\Entities $entities */
$entities = new Core\Feeds\Elastic\Entities();
$entities->setActor($currentUser);
$activities = $activities->map([$entities, 'cast']);
}
......
<?php
namespace Minds\Controllers\api\v2\admin\rewards;
use Minds\Api\Exportable;
use Minds\Core\Rewards\Withdraw\Repository;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Di\Di;
use Minds\Core\Rewards\Withdraw\Manager;
use Minds\Core\Rewards\Withdraw\Request;
use Minds\Entities\User;
use Minds\Interfaces;
use Minds\Api\Factory;
......@@ -11,32 +14,38 @@ class withdrawals implements Interfaces\Api, Interfaces\ApiAdminPam
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @param array $pages
* @return mixed|null
* @throws Exception
*/
public function get($pages)
{
$repository = new Repository();
$username = $_GET['user'];
/** @var Manager $manager */
$manager = Di::_()->get('Rewards\Withdraw\Manager');
if (!$username) {
return Factory::response([
'withdrawals' => [],
'load-next' => '',
]);
$userGuid = null;
if ($_GET['user']) {
$userGuid = (new User(strtolower($_GET['user'])))->guid;
}
$user = new User(strtolower($username));
$status = $_GET['status'] ?? null;
$withdrawals = $repository->getList([
$opts = [
'status' => $status,
'user_guid' => $userGuid,
'limit' => isset($_GET['limit']) ? (int) $_GET['limit'] : 12,
'offset' => isset($_GET['offset']) ? $_GET['offset'] : '',
'user_guid' => $user->guid
]);
'hydrate' => true,
'admin' => true,
];
/** @var Response $withdrawals */
$withdrawals = $manager->getList($opts);
return Factory::response([
'withdrawals' => Exportable::_($withdrawals['withdrawals']),
'load-next' => (string) base64_encode($withdrawals['token']),
'withdrawals' => $withdrawals,
'load-next' => $withdrawals->getPagingToken(),
]);
}
......@@ -57,6 +66,37 @@ class withdrawals implements Interfaces\Api, Interfaces\ApiAdminPam
*/
public function put($pages)
{
/** @var Manager $manager */
$manager = Di::_()->get('Rewards\Withdraw\Manager');
$request = $manager->get(
(new Request())
->setUserGuid((string) $pages[0] ?? null)
->setTimestamp((int) $pages[1] ?? null)
->setTx((string) $pages[2] ?? null)
);
if (!$request) {
return Factory::response([
'status' => 'error',
'message' => $errorMessage ?? 'Missing request',
]);
}
try {
$success = $manager->approve($request);
} catch (Exception $exception) {
$success = false;
$errorMessage = $exception->getMessage();
}
if (!$success) {
return Factory::response([
'status' => 'error',
'message' => $errorMessage ?? 'Cannot approve request',
]);
}
return Factory::response([]);
}
......@@ -67,6 +107,37 @@ class withdrawals implements Interfaces\Api, Interfaces\ApiAdminPam
*/
public function delete($pages)
{
/** @var Manager $manager */
$manager = Di::_()->get('Rewards\Withdraw\Manager');
$request = $manager->get(
(new Request())
->setUserGuid((string) $pages[0] ?? null)
->setTimestamp((int) $pages[1] ?? null)
->setTx((string) $pages[2] ?? null)
);
if (!$request) {
return Factory::response([
'status' => 'error',
'message' => $errorMessage ?? 'Missing request',
]);
}
try {
$success = $manager->reject($request);
} catch (Exception $exception) {
$success = false;
$errorMessage = $exception->getMessage();
}
if (!$success) {
return Factory::response([
'status' => 'error',
'message' => $errorMessage ?? 'Cannot reject request',
]);
}
return Factory::response([]);
}
}
......@@ -11,6 +11,14 @@ class analytics implements Interfaces\Api, Interfaces\ApiIgnorePam
{
public function get($pages)
{
// Temporary require admin
if (!Core\Session::isAdmin()) {
return Factory::response([
'status' => 'error',
'message' => 'Only admins can view these analytics. Use the dashboards instead.',
]);
}
if (!isset($pages[0])) {
return Factory::response([
'status' => 'error',
......
......@@ -123,10 +123,11 @@ class transactions implements Interfaces\Api
break;
case "withdraw":
$request = new Withdraw\Request();
$request->setTx($_POST['tx'])
$request
->setUserGuid(Session::getLoggedInUser()->guid)
->setAddress($_POST['address'])
->setTimestamp(time())
->setTx($_POST['tx'])
->setAddress($_POST['address'])
->setGas($_POST['gas'])
->setAmount((string) BigNumber::_($_POST['amount']));
......
......@@ -24,8 +24,8 @@ class canary implements Interfaces\Api
}
// Refresh the canary cookie
Di::_()->get('Features\Manager')
->setCanaryCookie($user->isCanary());
Di::_()->get('Features\Canary')
->setCookie($user->isCanary());
return Factory::response([
'enabled' => (bool) $user->isCanary(),
......@@ -57,8 +57,8 @@ class canary implements Interfaces\Api
]);
// Set the canary cookie
Di::_()->get('Features\Manager')
->setCanaryCookie($user->isCanary());
Di::_()->get('Features\Canary')
->setCookie($user->isCanary());
return Factory::response([]);
}
......
<?php
/**
* API for returning a captcha
*/
namespace Minds\Controllers\api\v2;
use Minds\Api\Factory;
use Minds\Common\Cookie;
use Minds\Core\Di\Di;
use Minds\Core\Config;
use Minds\Core\Session;
use Minds\Interfaces;
class captcha implements Interfaces\Api
{
public function get($pages)
{
$captchaManager = Di::_()->get('Captcha\Manager');
$captcha = $captchaManager->build();
return Factory::response(
$captcha->export()
);
}
public function post($pages)
{
return Factory::response([]);
}
public function put($pages)
{
return Factory::response([]);
}
public function delete($pages)
{
return Factory::response([]);
}
}
<?php
/**
* confirmation
*
* @author edgebal
*/
namespace Minds\Controllers\api\v2\email;
use Minds\Api\Factory;
use Minds\Core\Di\Di;
use Minds\Core\Email\Confirmation\Manager;
use Minds\Core\Session;
use Minds\Entities\User;
use Minds\Interfaces;
class confirmation implements Interfaces\Api
{
/**
* GET method
*/
public function get($pages)
{
return Factory::response([]);
}
/**
* POST method
*/
public function post($pages)
{
Factory::isLoggedIn();
/** @var User $user */
$user = Session::getLoggedinUser();
switch ($pages[0] ?? '') {
case 'resend':
try {
/** @var Manager $emailConfirmation */
$emailConfirmation = Di::_()->get('Email\Confirmation');
$emailConfirmation
->setUser($user)
->sendEmail();
return Factory::response([
'sent' => true
]);
} catch (\Exception $e) {
return Factory::response([
'status' => 'error',
'message' => $e->getMessage()
]);
}
break;
}
return Factory::response([]);
}
/**
* PUT method
*/
public function put($pages)
{
return Factory::response([]);
}
/**
* DELETE method
*/
public function delete($pages)
{
return Factory::response([]);
}
}
......@@ -24,6 +24,7 @@ class entities implements Interfaces\Api
public function get($pages)
{
$asActivities = (bool) ($_GET['as_activities'] ?? false);
$exportUserCounts = (bool) ($_GET['export_user_counts'] ?? false);
$urns = array_map([Urn::class, '_'], array_filter(explode(',', $_GET['urns'] ?? ''), [Urn::class, 'isValid']));
$resolver = new Resolver();
......@@ -31,11 +32,17 @@ class entities implements Interfaces\Api
->setUser(Session::getLoggedinUser() ?: null)
->setUrns($urns)
->setOpts([
'asActivities' => $asActivities
'asActivities' => $asActivities,
]);
$entities = $resolver->fetch();
if ($exportUserCounts) {
foreach ($entities as $user) {
$user->exportCounts = true;
}
}
// Return
return Factory::response([
'entities' => Exportable::_(array_values($entities)),
......
......@@ -49,6 +49,7 @@ class experiments implements Interfaces\Api
$_COOKIE['mexp'] = $cookieid;
}
/** @var Core\Experiments\Manager $manager */
$manager = Di::_()->get('Experiments\Manager');
if (Core\Session::isLoggedIn()) {
......
......@@ -4,14 +4,24 @@ namespace Minds\Controllers\api\v2;
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\Group;
use Minds\Entities\User;
use Minds\Interfaces;
class feeds implements Interfaces\Api
{
const PERIOD_FALLBACK = [
'12h' => '7d',
'24h' => '7d',
'7d' => '30d',
'30d' => '1y',
'1y' => 'all',
];
/**
* Gets a list of suggested hashtags, including the ones the user has opted in
* @param array $pages
......@@ -21,6 +31,9 @@ class feeds implements Interfaces\Api
{
Factory::isLoggedIn();
$now = time();
$periodsInSecs = Core\Feeds\Elastic\Repository::PERIODS;
/** @var User $currentUser */
$currentUser = Core\Session::getLoggedinUser();
......@@ -29,7 +42,7 @@ class feeds implements Interfaces\Api
if (!$filter) {
return Factory::response([
'status' => 'error',
'message' => 'Invalid filter'
'message' => 'Invalid filter',
]);
}
......@@ -38,7 +51,7 @@ class feeds implements Interfaces\Api
if (!$algorithm) {
return Factory::response([
'status' => 'error',
'message' => 'Invalid algorithm'
'message' => 'Invalid algorithm',
]);
}
......@@ -72,6 +85,12 @@ class feeds implements Interfaces\Api
$period = '1y';
}
$exportCounts = false;
if (isset($_GET['export_user_counts'])) {
$exportCounts = true;
}
//
$hardLimit = 600;
......@@ -119,6 +138,8 @@ class feeds implements Interfaces\Api
$sync = (bool) ($_GET['sync'] ?? false);
$periodFallback = (bool) ($_GET['period_fallback'] ?? false);
$asActivities = (bool) ($_GET['as_activities'] ?? true);
$query = isset($_GET['query']) ? urldecode($_GET['query']) : null;
......@@ -132,17 +153,18 @@ class feeds implements Interfaces\Api
if (!$container || !Core\Security\ACL::_()->read($container)) {
return Factory::response([
'status' => 'error',
'message' => 'Forbidden'
'message' => 'Forbidden',
]);
}
}
/** @var Core\Feeds\Top\Manager $manager */
$manager = Di::_()->get('Feeds\Top\Manager');
/** @var Core\Feeds\Elastic\Manager $manager */
$manager = Di::_()->get('Feeds\Elastic\Manager');
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
$entities->setActor($currentUser);
/** @var Core\Feeds\Elastic\Entities $elasticEntities */
$elasticEntities = new Core\Feeds\Elastic\Entities();
$elasticEntities
->setActor($currentUser);
$opts = [
'cache_key' => Core\Session::getLoggedInUserGuid(),
......@@ -185,22 +207,56 @@ class feeds implements Interfaces\Api
}
try {
$result = $manager->getList($opts);
$entities = new Response();
$fallbackAt = null;
$i = 0;
while ($entities->count() < $limit) {
$rows = $manager->getList($opts);
$entities = $entities->pushArray($rows->toArray());
if (
!$periodFallback ||
$opts['algorithm'] !== 'top' ||
!isset(static::PERIOD_FALLBACK[$opts['period']]) ||
in_array($opts['type'], ['user', 'group'], true) ||
++$i > 2 // Stop at 2nd fallback (i.e. 12h > 7d > 30d)
) {
break;
}
$period = $opts['period'];
$from = $now - $periodsInSecs[$period];
$opts['from_timestamp'] = $from * 1000;
$opts['period'] = static::PERIOD_FALLBACK[$period];
$opts['limit'] = $limit - $entities->count();
if (!$fallbackAt) {
$fallbackAt = $from;
}
}
if (!$sync) {
// Remove all unlisted content, if ES document is not in sync, it'll
// also remove pending activities
$result = $result->filter([$entities, 'filter']);
$entities = $entities->filter([$elasticEntities, 'filter']);
if ($asActivities) {
// Cast to ephemeral Activity entities, if another type
$result = $result->map([$entities, 'cast']);
$entities = $entities->map([$elasticEntities, 'cast']);
}
}
if ($type === 'user' && $exportCounts) {
foreach ($entities as $entity) {
$entity->getEntity()->exportCounts = true;
}
}
return Factory::response([
'status' => 'success',
'entities' => Exportable::_($result),
'entities' => Exportable::_($entities),
'fallback_at' => $fallbackAt,
'load-next' => $limit + $offset,
]);
} catch (\Exception $e) {
......
......@@ -100,6 +100,8 @@ class container implements Interfaces\Api
$forcePublic = (bool) ($_GET['force_public'] ?? false);
$reverseSort = (bool) ($_GET['reverse_sort'] ?? false);
$query = null;
if (isset($_GET['query'])) {
......@@ -108,11 +110,11 @@ class container implements Interfaces\Api
$custom_type = isset($_GET['custom_type']) && $_GET['custom_type'] ? [$_GET['custom_type']] : null;
/** @var Core\Feeds\Top\Manager $manager */
$manager = Di::_()->get('Feeds\Top\Manager');
/** @var Core\Feeds\Elastic\Manager $manager */
$manager = Di::_()->get('Feeds\Elastic\Manager');
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
/** @var Core\Feeds\Elastic\Entities $entities */
$entities = new Core\Feeds\Elastic\Entities();
$entities->setActor($currentUser);
$isOwner = false;
......@@ -133,6 +135,7 @@ class container implements Interfaces\Api
'period' => '1y',
'sync' => $sync,
'from_timestamp' => $fromTimestamp,
'reverse_sort' => $reverseSort,
'query' => $query,
'single_owner_threshold' => 0,
'pinned_guids' => $type === 'activity' ? array_reverse($container->getPinnedPosts()) : null,
......
......@@ -123,11 +123,11 @@ class scheduled implements Interfaces\Api
$custom_type = isset($_GET['custom_type']) && $_GET['custom_type'] ? [$_GET['custom_type']] : null;
/** @var Core\Feeds\Top\Manager $manager */
$manager = Di::_()->get('Feeds\Top\Manager');
/** @var Core\Feeds\Elastic\Manager $manager */
$manager = Di::_()->get('Feeds\Elastic\Manager');
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
/** @var Core\Feeds\Elastic\Entities $entities */
$entities = new Core\Feeds\Elastic\Entities();
$entities->setActor($currentUser);
$isOwner = false;
......@@ -151,7 +151,7 @@ class scheduled implements Interfaces\Api
'query' => $query,
'single_owner_threshold' => 0,
'pinned_guids' => $type === 'activity' ? array_reverse($container->getPinnedPosts()) : null,
'time_created_upper' => false,
'future' => true,
'owner_guid' => $currentUser->guid,
];
......
......@@ -85,11 +85,11 @@ class subscribed implements Interfaces\Api
$custom_type = isset($_GET['custom_type']) && $_GET['custom_type'] ? [$_GET['custom_type']] : null;
/** @var Core\Feeds\Top\Manager $manager */
$manager = Di::_()->get('Feeds\Top\Manager');
/** @var Core\Feeds\Elastic\Manager $manager */
$manager = Di::_()->get('Feeds\Elastic\Manager');
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
/** @var Core\Feeds\Elastic\Entities $entities */
$entities = new Core\Feeds\Elastic\Entities();
$entities->setActor($currentUser);
$opts = [
......
......@@ -51,7 +51,7 @@ class issues implements Interfaces\Api
$issue = new Issue;
$issue->setTitle($title)
->setDescription($description)
->setLabels('S - Triage (New), T - Bug (Triage)');
->setLabels('Status::Validation, Type::Bug (Triage)');
// call gitlab api
$res = $manager->postIssue($issue, $pages[0]);
......
......@@ -58,9 +58,7 @@ class upload implements Interfaces\Api
$lease->setGuid($guid)
->setMediaType($mediaType);
$manager
->setFullHD(Session::getLoggedinUser()->isPro())
->complete($lease);
$manager->complete($lease);
break;
}
return Factory::response([]);
......
<?php
/**
* Minds Video Controller
*
* @author Mark Harding
*/
namespace Minds\Controllers\api\v2\media;
use Minds\Api\Factory;
use Minds\Core\Di\Di;
use Minds\Core\Media\Video\Transcoder\TranscodeStates;
use Minds\Interfaces;
class video implements Interfaces\Api, Interfaces\ApiIgnorePam
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
*/
public function get($pages)
{
$videoManager = Di::_()->get('Media\Video\Manager');
$transcodeStates = Di::_()->get('Media\Video\Transcoder\TranscodeStates');
$video = $videoManager->get($pages[0]);
$sources = $videoManager->getSources($video);
$status = $transcodeStates->getStatus($video); // Currently not efficient as no caching
if ($status === TranscodeStates::FAILED && count($sources)) {
$status = TranscodeStates::COMPLETED;
}
Factory::response([
'entity' => $video->export(),
'sources' => Factory::exportable($sources),
'poster' => $video->getIconUrl(),
'transcode_status' => $status,
]);
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
*/
public function post($pages)
{
http_response_code(501);
exit;
}
/**
* Equivalent to HTTP PUT method
* @param array $pages
* @return mixed|null
*/
public function put($pages)
{
http_response_code(501);
exit;
}
/**
* Equivalent to HTTP DELETE method
* @param array $pages
* @return mixed|null
*/
public function delete($pages)
{
http_response_code(501);
exit;
}
}
......@@ -16,6 +16,13 @@ class appeals implements Interfaces\Api
{
public function get($pages)
{
if (!Core\Session::isLoggedIn()) {
return Factory::response([
'status' => 'error',
'message' => 'You must be loggedin',
]);
}
if ($_POST['offset']) {
return Factory::response([ ]);
}
......
......@@ -14,6 +14,7 @@ use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\Response\SapiEmitter;
use Sentry;
class token implements Interfaces\Api, Interfaces\ApiIgnorePam
{
......@@ -70,9 +71,10 @@ class token implements Interfaces\Api, Interfaces\ApiIgnorePam
$refreshTokenRepository->revokeRefreshToken($tokenId);
$response = new JsonResponse([]);
} catch (\Exception $e) {
Sentry\captureException($e); // Log to sentry
$body = [
'status' => 'error',
'message' => $exception->getMessage(),
'message' => $e->getMessage(),
];
$response = new JsonResponse($body, 500);
}
......
......@@ -3,6 +3,7 @@
namespace Minds\Controllers\api\v2\onboarding;
use Minds\Api\Factory;
use Minds\Core\Di\Di;
use Minds\Core\Onboarding\Manager;
use Minds\Core\Session;
use Minds\Interfaces;
......@@ -11,7 +12,7 @@ class progress implements Interfaces\Api
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @param array $pages
* @return mixed|null
* @throws \Exception
*/
......@@ -23,11 +24,20 @@ class progress implements Interfaces\Api
$manager = new Manager();
$manager->setUser(Session::getLoggedInUser());
/** @var \Minds\Core\Features\Manager $manager */
$featuresManager = Di::_()->get('Features\Manager');
if ($featuresManager->has('onboarding-december-2019')) {
return Factory::response([
'show_onboarding' => !$manager->wasOnboardingShown(),
]);
}
$allItems = $manager->getAllItems();
$completedItems = $manager->getCompletedItems();
return Factory::response([
'show_onboarding' => !$manager->wasOnboardingShown() && count($allItems) > count($completedItems) ,
'show_onboarding' => !$manager->wasOnboardingShown() && count($allItems) > count($completedItems),
'all_items' => $allItems,
'completed_items' => $completedItems,
'creator_frequency' => $manager->getCreatorFrequency(),
......@@ -36,7 +46,7 @@ class progress implements Interfaces\Api
/**
* Equivalent to HTTP POST method
* @param array $pages
* @param array $pages
* @return mixed|null
* @throws \Exception
*/
......@@ -47,7 +57,7 @@ class progress implements Interfaces\Api
/**
* Equivalent to HTTP PUT method
* @param array $pages
* @param array $pages
* @return mixed|null
*/
public function put($pages)
......@@ -57,7 +67,7 @@ class progress implements Interfaces\Api
/**
* Equivalent to HTTP DELETE method
* @param array $pages
* @param array $pages
* @return mixed|null
*/
public function delete($pages)
......
......@@ -12,7 +12,7 @@ use Minds\Core\Session;
use Minds\Interfaces;
use Minds\Core\Payments\Stripe;
class photoid implements Interfaces\Api
class document implements Interfaces\Api
{
public function get($pages)
{
......@@ -21,11 +21,20 @@ class photoid implements Interfaces\Api
public function post($pages)
{
$documentType = $pages[0] ?? null;
if (!$documentType) {
return Factory::response([
'status' => 'error',
'message' => '/:documentType must be provided',
]);
}
$user = Session::getLoggedInUser();
$connectManager = new Stripe\Connect\Manager();
$account = $connectManager->getByUser($user);
$fp = fopen($_FILES['file']['tmp_name'], 'r');
$connectManager->addPhotoId($account, $fp);
$connectManager->addDocument($account, $fp, $documentType);
return Factory::response([ 'account_id' => $account->getId() ]);
}
......
<?php
/**
*
*/
namespace Minds\Controllers\api\v2\payments\stripe\connect;
use Minds\Api\Factory;
use Minds\Common\Cookie;
use Minds\Core\Di\Di;
use Minds\Core\Config;
use Minds\Core\Session;
use Minds\Interfaces;
use Minds\Core\Payments\Stripe;
class update implements Interfaces\Api
{
public function get($pages)
{
return Factory::response([]);
}
public function post($pages)
{
$user = Session::getLoggedInUser();
$connectManager = new Stripe\Connect\Manager();
$account = $connectManager->getByUser($user);
if ($_POST['phone'] ?? null) {
$account->setPhoneNumber($_POST['phone']);
}
if ($_POST['id_number'] ?? null) {
$account->setPersonalIdNumber($_POST['id_number']);
}
try {
$connectManager->update($account);
} catch (\Exception $e) {
return Factory::response([
'status' => 'error',
'message' => $e->getMessage(),
]);
}
return Factory::response([]);
}
public function put($pages)
{
return Factory::response([]);
}
public function delete($pages)
{
return Factory::response([]);
}
}
......@@ -34,13 +34,15 @@ class channel implements Interfaces\Api
$channel->fullExport = true; //get counts
$channel->exportCounts = true;
if (!$channel->isPro()) {
if (!$channel->isPro() && $channel->getGuid() !== $currentUser->getGuid()) {
return Factory::response([
'status' => 'error',
'message' => 'E_NOT_PRO'
]);
}
$currentUser = Session::getLoggedinUser();
/** @var Manager $manager */
$manager = Di::_()->get('Pro\Manager');
$manager->setUser($channel);
......
......@@ -11,6 +11,7 @@ use Minds\Common\Repository\Response;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Entities\Factory as EntitiesFactory;
use Minds\Entities\Group;
use Minds\Entities\User;
use Minds\Interfaces;
......@@ -24,7 +25,6 @@ class content implements Interfaces\Api
$currentUser = Core\Session::getLoggedinUser();
$container_guid = $pages[0] ?? null;
$owner_guid = null;
if (!$container_guid) {
return Factory::response([
......@@ -43,9 +43,12 @@ class content implements Interfaces\Api
}
$type = '';
$algorithm = strtolower($_GET['algorithm'] ?? 'top');
switch ($pages[1]) {
case 'activities':
$type = 'activity';
$algorithm = 'latest';
break;
case 'images':
$type = 'object:image';
......@@ -58,8 +61,6 @@ class content implements Interfaces\Api
break;
case 'groups':
$type = 'group';
$container_guid = null;
$owner_guid = $pages[0];
break;
case 'all':
$type = 'all';
......@@ -108,8 +109,8 @@ class content implements Interfaces\Api
$query = $_GET['query'];
}
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
/** @var Core\Feeds\Elastic\Entities $entities */
$entities = new Core\Feeds\Elastic\Entities();
$entities->setActor($currentUser);
$isOwner = false;
......@@ -122,12 +123,12 @@ class content implements Interfaces\Api
$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,
'offset' => $offset,
'type' => $type,
'algorithm' => 'top',
'algorithm' => $algorithm,
'period' => '7d',
'sync' => $sync,
'from_timestamp' => $fromTimestamp,
......@@ -159,7 +160,11 @@ class content implements Interfaces\Api
try {
$result = $this->getData($entities, $opts, $asActivities, $sync);
if ($result->count() <= static::MIN_COUNT) {
if (
$opts['algorithm'] !== 'latest' &&
$opts['type'] !== 'group' &&
$result->count() <= static::MIN_COUNT
) {
$opts['algorithm'] = 'latest';
$result = $this->getData($entities, $opts, $asActivities, $sync);
}
......@@ -178,7 +183,7 @@ class content implements Interfaces\Api
}
/**
* @param Core\Feeds\Top\Entities $entities
* @param Core\Feeds\Elastic\Entities $entities
* @param array $opts
* @param bool $asActivities
* @param bool $sync
......@@ -187,20 +192,34 @@ class content implements Interfaces\Api
*/
private function getData($entities, $opts, $asActivities, $sync)
{
switch ($opts['type']) {
case 'group':
/** @var Core\Groups\Ownership $manager */
$manager = Di::_()->get('Groups\Ownership');
$result = $manager
->setUserGuid($opts['container_guid'])
->fetch();
/** @var Core\Feeds\Top\Manager $manager */
$manager = Di::_()->get('Feeds\Top\Manager');
$result = $manager->getList($opts);
break;
if (!$sync) {
// Remove all unlisted content, if ES document is not in sync, it'll
// also remove pending activities
$result = $result->filter([$entities, 'filter']);
default:
/** @var Core\Feeds\Elastic\Manager $manager */
$manager = Di::_()->get('Feeds\Elastic\Manager');
$result = $manager->getList($opts);
if ($asActivities) {
// Cast to ephemeral Activity entities, if another type
$result = $result->map([$entities, 'cast']);
}
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']);
}
}
break;
}
return $result;
......
......@@ -75,14 +75,14 @@ class settings implements Interfaces\Api
->setUser($user)
->setActor(Session::getLoggedinUser());
if (!$manager->isActive()) {
return Factory::response([
'status' => 'error',
'message' => 'You are not Pro',
]);
}
if (isset($_POST['domain'])) {
// if (!$manager->isActive()) {
// return Factory::response([
// 'status' => 'error',
// 'message' => 'You are not Pro',
// ]);
// }
if (isset($_POST['domain']) && $manager->isActive()) {
/** @var ProDomain $proDomain */
$proDomain = Di::_()->get('Pro\Domain');
......
......@@ -85,12 +85,12 @@ class assets implements Interfaces\Api
->setUser($user)
->setActor(Session::getLoggedinUser());
if (!$manager->isActive()) {
return Factory::response([
'status' => 'error',
'message' => 'You are not Pro',
]);
}
// if (!$manager->isActive()) {
// return Factory::response([
// 'status' => 'error',
// 'message' => 'You are not Pro',
// ]);
// }
/** @var AssetsManager $assetsManager */
$assetsManager = Di::_()->get('Pro\Assets\Manager');
......
......@@ -4,6 +4,7 @@ namespace Minds\Controllers\api\v2\settings;
use Minds\Api\Factory;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Core\Email\Confirmation\Manager as EmailConfirmation;
use Minds\Core\Email\EmailSubscription;
use Minds\Entities\User;
use Minds\Interfaces;
......@@ -67,8 +68,27 @@ class emails implements Interfaces\Api
]);
}
$emailChange = strtolower($_POST['email']) !== strtolower($user->getEmail());
$user->setEmail($_POST['email']);
$user->save();
if ($emailChange) {
/** @var EmailConfirmation $emailConfirmation */
$emailConfirmation = Di::_()->get('Email\Confirmation');
$emailConfirmation
->setUser($user);
$reset = $emailConfirmation
->reset();
if ($reset) {
$emailConfirmation
->sendEmail();
} else {
error_log('Cannot reset email confirmation for ' . $user->guid);
}
}
}
if (isset($_POST['notifications'])) {
......
<?php
/**
* authorize
* @author edgebal
*/
namespace Minds\Controllers\api\v2\sso;
use Exception;
use Minds\Api\Factory;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\SSO\Manager;
use Minds\Entities\User;
use Minds\Interfaces;
use Zend\Diactoros\ServerRequest;
class authorize implements Interfaces\Api, Interfaces\ApiIgnorePam
{
/** @var ServerRequest */
public $request;
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
*/
public function get($pages)
{
return Factory::response([]);
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
* @throws Exception
*/
public function post($pages)
{
$origin = $this->request->getServerParams()['HTTP_ORIGIN'] ?? '';
if (!$origin) {
return Factory::response([
'status' => 'error',
'message' => 'No HTTP Origin header'
]);
}
$domain = parse_url($origin, PHP_URL_HOST);
/** @var Manager $sso */
$sso = Di::_()->get('SSO');
$sso
->setDomain($domain);
try {
$sso
->authorize($_POST['token']);
} catch (Exception $e) {
error_log((string) $e);
return Factory::response([
'status' => 'error',
'message' => 'Cannot authorize',
]);
}
/** @var User $currentUser */
$currentUser = Session::getLoggedinUser();
return Factory::response([
'user' => $currentUser ? $currentUser->export() : null,
]);
}
/**
* 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
/**
* connect
* @author edgebal
*/
namespace Minds\Controllers\api\v2\sso;
use Exception;
use Minds\Api\Factory;
use Minds\Core\Di\Di;
use Minds\Core\SSO\Manager;
use Minds\Interfaces;
use Zend\Diactoros\ServerRequest;
class connect implements Interfaces\Api, Interfaces\ApiIgnorePam
{
/** @var ServerRequest */
public $request;
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
*/
public function get($pages)
{
return Factory::response([]);
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
* @throws Exception
*/
public function post($pages)
{
$origin = $this->request->getServerParams()['HTTP_ORIGIN'] ?? '';
if (!$origin) {
return Factory::response([
'status' => 'error',
'message' => 'No HTTP Origin header'
]);
}
$domain = parse_url($origin, PHP_URL_HOST);
/** @var Manager $sso */
$sso = Di::_()->get('SSO');
$sso
->setDomain($domain);
try {
return Factory::response([
'token' => $sso->generateToken()
]);
} catch (Exception $e) {
error_log((string) $e);
return Factory::response([
'status' => 'error',
'message' => 'Cannot connect',
]);
}
}
/**
* 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([]);
}
}
......@@ -8,6 +8,7 @@
namespace Minds\Controllers\api\v2\wallet\usd;
use Minds\Core;
use Minds\Core\Config;
use Minds\Helpers;
use Minds\Interfaces;
use Minds\Api\Factory;
......@@ -103,11 +104,13 @@ class account implements Interfaces\Api
Factory::isLoggedIn();
$response = [];
$vars = Core\Router::getPutVars();
$vars = Core\Router\PrePsr7\Router::getPutVars();
$user = Core\Session::getLoggedInUser();
$account = (new Payments\Stripe\Connect\Account())
->setUserGuid(Core\Session::getLoggedInUser()->guid)
->setUser(Core\Session::getLoggedInUser())
->setUserGuid($user->guid)
->setUser($user)
->setDestination('bank')
->setCountry($vars['country'])
->setSSN($vars['ssn'] ? str_pad((string) $vars['ssn'], 4, '0', STR_PAD_LEFT) : '')
......@@ -121,7 +124,9 @@ class account implements Interfaces\Api
->setState($vars['state'])
->setPostCode($vars['postCode'])
->setPhoneNumber($vars['phoneNumber'])
->setIp($_SERVER['HTTP_X_FORWARDED_FOR']);
->setIp($_SERVER['HTTP_X_FORWARDED_FOR'])
->setEmail($user->getEmail())
->setUrl(Config::_()->get('site_url') . $user->username);
try {
$stripeConnectManager = Core\Di\Di::_()->get('Stripe\Connect\Manager');
......
......@@ -59,6 +59,13 @@ class wire implements Interfaces\Api
$amount = BigNumber::_($_POST['amount']);
$recurring = isset($_POST['recurring']) ? $_POST['recurring'] : false;
$recurringInterval = $_POST['recurring_interval'] ?? 'once';
if ($recurring && $recurringInterval === 'once') {
$recurringInterval = 'monthly';
// Client side bug we need to track down, so lets log in Sentry
\Sentry\captureMessage("Recurring Subscription was created with 'once' interval");
}
if (!$amount) {
return Factory::response(['status' => 'error', 'message' => 'you must send an amount']);
......@@ -80,6 +87,7 @@ class wire implements Interfaces\Api
$manager
->setAmount((string) BigNumber::toPlain($amount, $digits))
->setRecurring($recurring)
->setRecurringInterval($recurringInterval)
->setSender(Core\Session::getLoggedInUser())
->setEntity($entity)
->setPayload((array) $_POST['payload']);
......
......@@ -24,14 +24,15 @@ class deeplinks implements Interfaces\Api, Interfaces\ApiIgnorePam
[
'appID' => "35U3998VRZ.com.minds.mobile",
'paths' => [
'/groups/profile/*',
'/groups/*',
'/media/*',
'/newsfeed/*',
'/blog/view/*',
'/blog/*',
'/channels/*',
'/*'
'/email-confirmation'
// '/groups/profile/*',
// '/groups/*',
// '/media/*',
// '/newsfeed/*',
// '/blog/view/*',
// '/blog/*',
// '/channels/*',
// '/*'
]
]
]
......
......@@ -31,6 +31,10 @@ class pro implements Interfaces\FS
$contents = $file->read();
if (!$contents) {
$this->fallback($pages);
}
header(sprintf("Content-Type: %s", $asset->getMimeType()));
header(sprintf("Expires: %s", date('r', time() + 864000)));
header('Pragma: public');
......@@ -39,4 +43,25 @@ class pro implements Interfaces\FS
echo $contents;
exit;
}
/**
* Fallback
* @param array $pages
* @return void
*/
private function fallback($pages): void
{
switch ($pages[1]) {
case "background":
$bannersFs = new banners();
$bannersFs->get([ $pages[0] ]);
exit;
break;
case "logo":
$avatarsFs = new avatars();
$avatarsFs->get([ $pages[0], 'large' ]);
exit;
break;
}
}
}
......@@ -95,7 +95,7 @@ class EarningsDashboard implements DashboardInterface
return [
'category' => 'earnings',
'label' => 'Pro Earnings',
'description' => 'Earnings for PRO members will be paid out within 30 days upon reaching a minumum balance of $100.00.',
'description' => 'Earnings for Pro members will be paid out within 30 days upon reaching a minumum balance of $100.00.',
'timespan' => $this->timespansCollection->getSelected()->getId(),
'timespans' => $this->timespansCollection->export(),
'metric' => $this->metricsCollection->getSelected()->getId(),
......
......@@ -77,7 +77,8 @@ class EngagementDashboard implements DashboardInterface
new Metrics\Engagement\VotesUpMetric(),
new Metrics\Engagement\CommentsMetric(),
new Metrics\Engagement\RemindsMetric(),
new Metrics\Engagement\SubscribersMetric()
new Metrics\Engagement\SubscribersMetric(),
new Metrics\Engagement\ReferralsMetric()
)
->build();
......
......@@ -17,7 +17,7 @@ class SalesEarningsMetric extends AbstractEarningsMetric
protected $label = 'Sales';
/** @var string */
protected $description = "Total earnings for the sales you have referred. You earn a 25% commission when your referrals purchase Plus, Pro or Minds Tokens.";
protected $description = "Total earnings for the sales you have referred. You earn a 25% commission when your referrals purchase Pro or Minds Tokens.";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
......
......@@ -17,7 +17,7 @@ class ViewsEarningsMetric extends AbstractEarningsMetric
protected $label = 'Pageviews';
/** @var string */
protected $description = "Total earnings for the pageviews on your channel's assets. You earn $1 for every 1,000 pageviews.";
protected $description = "Total earnings for the pageviews on your channel’s assets. You earn a variable RPM for every 1,000 pageviews.";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
......
......@@ -7,6 +7,8 @@ use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
use Minds\Core\Analytics\Dashboards\Metrics\HistogramSegment;
use Minds\Core\Analytics\Dashboards\Metrics\HistogramBucket;
abstract class AbstractEngagementMetric extends AbstractMetric
{
......@@ -31,6 +33,9 @@ abstract class AbstractEngagementMetric extends AbstractMetric
/** @var string */
protected $aggField = '';
/** @var HistogramSegment[] */
protected $segments = [];
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
......@@ -118,6 +123,15 @@ abstract class AbstractEngagementMetric extends AbstractMetric
*/
public function buildVisualisation(): self
{
// This is for backwards compatability. We should put deprecated notice here soon
if (empty($this->segments)) {
$this->segments = [
(new HistogramSegment)
->setAggField($this->aggField)
->setAggType('sum'),
];
}
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
......@@ -140,10 +154,20 @@ abstract class AbstractEngagementMetric extends AbstractMetric
$must[] = [
'exists' => [
'field' => $this->aggField,
'field' => $this->segments[0]->getAggField(),
],
];
$aggs = [];
foreach ($this->segments as $i => $segment) {
$key = (string) $i + 2;
$aggs[$key] = [
$segment->getAggType() => [
'field' => $segment->getAggField(),
],
];
}
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
......@@ -165,18 +189,12 @@ abstract class AbstractEngagementMetric extends AbstractMetric
'max' => time() * 1000,
],
],
'aggs' => [
'2' => [
'sum' => [
'field' => $this->aggField,
],
],
],
'aggs' => $aggs,
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
......@@ -185,17 +203,22 @@ abstract class AbstractEngagementMetric extends AbstractMetric
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
foreach ($this->segments as $i => $segment) {
$key = (string) $i + 2;
$segment->addBucket(
(new HistogramBucket)
->setKey($bucket['key'])
->setTimestampMs($bucket['key'])
->setValue($bucket[$key]['value'])
);
}
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXLabel('Date')
->setYLabel('Count')
->setBuckets($buckets);
->setSegments($this->segments);
return $this;
}
......
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Engagement;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class ReferralsActiveMetric extends AbstractEngagementMetric
{
/** @var string */
protected $id = 'referrals_active';
/** @var string */
protected $label = 'Active Referrals';
/** @var string */
protected $description = "Referred users who are active for more than 50% of their first 7 days";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'referral::active';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Engagement;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
use Minds\Core\Analytics\Dashboards\Metrics\HistogramSegment;
class ReferralsMetric extends AbstractEngagementMetric
{
public function __construct($es = null)
{
parent::__construct($es);
$this->segments = [
(new HistogramSegment())
->setAggField('referral::total')
->setAggType('sum'),
(new HistogramSegment())
->setAggField('referral::active')
->setAggType('sum')
->setLabel('Active'),
];
}
/** @var string */
protected $id = 'referrals';
/** @var string */
protected $label = 'Referrals';
/** @var string */
protected $description = "Number of comments you have received on your content";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'referral::total';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Traits\MagicAttributes;
/**
* @method HistogramBucket setKey(string $key)
* @method string getKey()
* @method HistogramBucket setTimestampMs(int $ms)
* @method int getTimestampMs()
* @method HistogramBucket setValue(mixed $value)
* @method mixed $value
*/
class HistogramBucket
{
use MagicAttributes;
/** @var string */
protected $key;
/** @var int */
protected $timestampMs;
/** @var mixed */
protected $value;
/**
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
return [
'key' => $this->key,
'date' => date('c', $this->timestampMs / 1000),
'value' => $this->value,
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Traits\MagicAttributes;
/**
* @method HistogramSegment setLabel(string $label)
* @method string getLabel()
* @method HistogramSegment setAggField(string $field)
* @method string getAggField()
* @method HistogramSegment setAggType(string $type)
* @method string getAggType()
* @method HistogramSegment setComparison(bool $is)
* @method bool isComparison()
* @method HistogramSegment setBuckets(array $buckets)
* @method array getBuckets()
*/
class HistogramSegment
{
use MagicAttributes;
/** @var string */
protected $label;
/** @var string */
protected $aggField;
/** @var string */
protected $aggType = 'sum';
/** @var bool */
protected $comparison = false;
/** @var HistogramBucket[] */
protected $buckets = [];
/**
* @param array
* @return self
*/
public function addBucket(HistogramBucket $bucket): self
{
$this->buckets[] = $bucket;
return $this;
}
/**
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
return [
'label' => $this->label,
'comparison' => (bool) $this->comparison,
'buckets' => array_map(function ($bucket) {
if (is_array($bucket)) { // TODO: throw deprecated error because should be HistogramBucket
return $bucket;
}
return $bucket->export();
}, $this->buckets),
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
use Minds\Core\Analytics\Dashboards\Metrics\HistogramSegment;
use Minds\Traits\MagicAttributes;
class ChartVisualisation extends AbstractVisualisation
......@@ -15,14 +16,11 @@ class ChartVisualisation extends AbstractVisualisation
/** @var string */
private $xLabel = 'Date';
/** @var array */
private $xValues;
/** @var string */
private $yLabel = 'count';
/** @var array */
private $yValues;
/** @var HistogramSegment[] */
private $segments;
/** @var array */
private $buckets = [];
......@@ -34,13 +32,18 @@ class ChartVisualisation extends AbstractVisualisation
*/
public function export(array $extras = []): array
{
if (!$this->segments) { // TODO: show deprecated message as we should use segments now
$this->segments = [
(new HistogramSegment())
->setBuckets($this->buckets),
];
}
return [
'type' => $this->type,
'segments' => [
[
'buckets' => (array) $this->buckets,
],
]
'segments' => array_map(function ($segment) {
return $segment->export();
}, $this->segments),
];
}
}
......@@ -64,7 +64,11 @@ class EngagementSynchroniser
->setResolution('day');
foreach ($buckets['metrics']['buckets'] as $metrics) {
$record->incrementSum($metrics['key'] . '::total', (int) $metrics['doc_count']);
$aggType = 'total';
if ($metrics['key'] === 'referral') {
$aggType = 'rewards';
}
$record->incrementSum("{$metrics['key']}::{$aggType}", (int) $metrics['doc_count']);
}
$this->records[] = $record;
++$i;
......
......@@ -16,6 +16,7 @@ class Manager
EngagementSynchroniser::class,
PartnerEarningsSynchroniser::class,
SignupsSynchroniser::class,
ReferralsSynchroniser::class,
ActiveUsersSynchroniser::class,
ViewsSynchroniser::class,
];
......@@ -52,9 +53,10 @@ class Manager
/**
* Synchronise views from cassandra to elastic
* @param array $opts
* @return iterable
*/
public function sync(): iterable
public function sync(array $opts = []): iterable
{
foreach (Manager::SYNCHRONISERS as $synchroniserClass) {
$synchroniser = new $synchroniserClass;
......
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Di\Di;
use Minds\Core\Analytics\UserStates\ActiveUsersIterator;
use DateTime;
use Exception;
class ReferralsSynchroniser
{
/** @var array */
private $records = [];
/** @var ElasticSearch\Client */
private $es;
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
$this->entitiesBuilder = $entitiesBuilder ?? Di::_()->get('EntitiesBuilder');
}
/**
* @param int $from
* @return self
*/
public function setFrom($from): self
{
$this->from = $from;
return $this;
}
/**
* Convert to records
* @return iterable
*/
public function toRecords(): iterable
{
$i = 0;
foreach ($this->getReferralCounts([ 'referrers' => true ]) as $bucket) {
$urn = "urn:user:{$bucket['key']}";
$count = $bucket['doc_count'];
$record = new EntityCentricRecord();
$record->setEntityUrn($urn)
->setOwnerGuid($bucket['key'])
->setTimestamp($this->from)
->setResolution('day');
$record->incrementSum('referral::total', (int) $count);
$this->records[] = $record;
++$i;
error_log("Referrals (total): $i");
}
// TODO: now get the users who are active
$i = 0;
foreach ($this->getActiveCounts() as $bucket) {
$urn = "urn:user:{$bucket['referrerGuid']}";
$count = $bucket['count'];
$record = new EntityCentricRecord();
$record->setEntityUrn($urn)
->setOwnerGuid($bucket['referrerGuid'])
->setTimestamp($bucket['timestamp'])
->setResolution('day');
$record->incrementSum('referral::active', $count);
$this->records[] = $record;
++$i;
error_log("Referrals (active): $i [$count]");
}
foreach ($this->records as $record) {
yield $record;
}
}
/**
* Return the counts of referrals
* @param array $opts
* @return iterable
*/
private function getReferralCounts(array $opts): iterable
{
$opts = array_merge([
'from' => $this->from,
'referrers' => true,
], $opts);
$must = [];
$must[] = [
'term' => [
'action.keyword' => 'signup',
],
];
$must[] = [
'exists' => [
'field' => 'referrer_guid',
],
];
$must[] = [
'range' => [
'@timestamp' => [
'gte' => $opts['from'] * 1000,
'lt' => strtotime('+1 day', $opts['from']) * 1000,
],
],
];
$partition = 0;
$partitions = 50;
$partitionSize = 5000; // Allows for 250,000 entities
$index = 'minds-metrics-*';
while (++$partition < $partitions) {
// Do the query
$query = [
'index' => $index,
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'terms' => [
'field' => $opts['referrers'] ? 'referrer_guid' : 'user_guid.keyword',
'min_doc_count' => 1,
'size' => $partitionSize,
'include' => [
'partition' => $partition,
'num_partitions' => $partitions,
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
yield $bucket;
}
}
}
/**
* To get active users we go back 7 days and collect their daily activity buckets
* If they are active for 4/7 days then they are active
* @return iterable
*/
private function getActiveCounts(): iterable
{
$referrers = [];
$activeUsersIterator = new ActiveUsersIterator();
$signupDate = strtotime('-7 days', $this->from);
$userGuids = array_map(function ($bucket) {
return $bucket['key'];
}, iterator_to_array($this->getReferralCounts([
'referrers' => false,
'from' => $signupDate,
]))); // false returns referrees instead of referrers
$activeUsersIterator
->setReferenceDate(strtotime('+1 day', $this->from))
->setRangeOffset(7) // Go back 7 days
->setFilterUserGuids($userGuids);
foreach ($activeUsersIterator as $bucket) {
$pct = $bucket->getActivityPercentage();
if ($pct < 0.5) {
continue;
}
$user = $this->entitiesBuilder->single($bucket->getUserGuid());
if ($user->referrer) {
$referrers[$user->referrer] = ($referrers[$user->referrer] ?? 0) + 1;
error_log("Referral active: {$bucket->getUserGuid()} referrer: $user->referrer total: {$referrers[$user->referrer]} joined: " . date('c', $user->time_created));
}
}
foreach ($referrers as $referrer => $count) {
yield [
'referrerGuid' => $referrer,
'timestamp' => $signupDate,
'count' => $count,
];
}
}
}
......@@ -39,7 +39,7 @@ class SignupsSynchroniser
$days = (int) $date->diff($now)->format('%a');
foreach ($this->signupMetric->get($days) as $bucket) {
error_log($bucket['date']);
error_log("Signups (total {$bucket['date']}) {$bucket['total']}");
$record = new EntityCentricRecord();
$record->setEntityUrn("urn:metric:global")
->setOwnerGuid((string) 0) // Site is owner
......
......@@ -24,6 +24,9 @@ class ActiveUsersIterator implements \Iterator
private $active;
private $valid = true;
/** @var array */
private $filterUserGuids;
public function __construct($client = null)
{
$this->client = $client ?: Di::_()->get('Database\ElasticSearch');
......@@ -48,6 +51,12 @@ class ActiveUsersIterator implements \Iterator
return $this;
}
public function setFilterUserGuids(array $userGuids): self
{
$this->filterUserGuids = $userGuids;
return $this;
}
//Builds up a sub aggregate that counts the days for a bucket with the same name
private function buildBucketCountAggregation($name)
{
......@@ -99,21 +108,55 @@ class ActiveUsersIterator implements \Iterator
$bucketAggregations["day-$dayOffset"] = $this->buildBucketCountAggregation("day-$dayOffset");
}
$must = [
['match_phrase' => [
'action.keyword' => [
'query' => 'active',
],
]],
['range' => [
'@timestamp' => [
$must = [];
$must[] = [
'terms' => [
'action.keyword' => [ 'active', 'signup' ],
],
];
$must[] = [
'range' => [
'@timestamp' => [
'from' => $from * 1000, //midnight of the first day
'to' => $to * 1000, //midnight of the last day
'format' => 'epoch_millis',
],
]],
],
]
];
if ($this->filterUserGuids) {
$must[] = [
'terms' => [
'user_guid.keyword' => $this->filterUserGuids,
],
];
}
/*}
$bool = [
'should' => [
[
'match' => [
'action.keyword' => 'signup',
],
],
[
'exists' => [
'field' => 'referrer_guid',
],
],
],
'must' => [
$must[1], // We copy the range
],
"minimum_should_match" => 2
];
$must = [
'bool' => $bool,
];
}*/
//split up users by user guid
$aggs = [
'users' => [
......
......@@ -8,30 +8,39 @@
namespace Minds\Core\Blockchain\Events;
use Minds\Core\Blockchain\Contracts\MindsToken;
use Minds\Core\Blockchain\Transactions\Manager;
use Exception;
use Minds\Core\Blockchain\Transactions\Repository as TransactionsRepository;
use Minds\Core\Blockchain\Transactions\Transaction;
use Minds\Core\Blockchain\Util;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Rewards\Withdraw;
use Minds\Core\Rewards\Withdraw\Manager;
use Minds\Core\Rewards\Withdraw\Request;
use Minds\Core\Util\BigNumber;
class WithdrawEvent implements BlockchainEventInterface
{
/** @var array $eventsMap */
/** @var array */
public static $eventsMap = [
'0x317c0f5ab60805d3e3fb6aaa61ccb77253bbb20deccbbe49c544de4baa4d7f8f' => 'onRequest',
'blockchain:fail' => 'withdrawFail',
];
/** @var Manager $manager */
private $manager;
/** @var Manager */
protected $manager;
/** @var Repository $repository **/
/** @var TransactionsRepository **/
protected $txRepository;
/** @var Config $config */
private $config;
/** @var Config */
protected $config;
/**
* WithdrawEvent constructor.
* @param Manager $manager
* @param TransactionsRepository $txRepository
* @param Config $config
*/
public function __construct($manager = null, $txRepository = null, $config = null)
{
$this->txRepository = $txRepository ?: Di::_()->get('Blockchain\Transactions\Repository');
......@@ -50,30 +59,31 @@ class WithdrawEvent implements BlockchainEventInterface
/**
* @param $topic
* @param array $log
* @throws \Exception
* @param $transaction
* @throws Exception
*/
public function event($topic, array $log, $transaction)
{
$method = static::$eventsMap[$topic];
if ($log['address'] != $this->config->get('blockchain')['contracts']['withdraw']['contract_address']) {
throw new \Exception('Event does not match address');
throw new Exception('Event does not match address');
}
if (method_exists($this, $method)) {
$this->{$method}($log, $transaction);
} else {
throw new \Exception('Method not found');
throw new Exception('Method not found');
}
}
public function onRequest($log, $transaction)
public function onRequest($log, Transaction $transaction)
{
$address = $log['address'];
if ($address != $this->config->get('blockchain')['contracts']['withdraw']['contract_address']) {
$this->withdrawFail($log, $transaction);
throw new \Exception('Incorrect address sent the withdraw event');
throw new Exception('Incorrect address sent the withdraw event');
}
$tx = $log['transactionHash'];
......@@ -82,29 +92,43 @@ class WithdrawEvent implements BlockchainEventInterface
$gas = (string) BigNumber::fromHex($gas);
$amount = (string) BigNumber::fromHex($amount);
//double check the details of this transaction match with what the user actually requested
$request = new Withdraw\Request();
$request
->setTx($tx)
->setAddress($address)
->setUserGuid($user_guid)
->setGas($gas)
->setTimestamp($transaction->getTimestamp())
->setAmount($amount);
try {
$this->manager->complete($request, $transaction);
} catch (\Exception $e) {
error_log(print_r($e, true));
$request = $this->manager->get(
(new Request())
->setUserGuid($user_guid)
->setTimestamp($transaction->getTimestamp())
->setTx($tx)
);
if (!$request) {
throw new \Exception('Unknown withdrawal');
}
if ((string) $address !== (string) $request->getAddress()) {
throw new \Exception('Wrong address value');
} elseif ((string) $gas !== (string) $request->getGas()) {
throw new \Exception('Wrong gas value');
} elseif ((string) $amount !== (string) $request->getAmount()) {
throw new \Exception('Wrong amount value');
}
$this->manager->confirm($request, $transaction);
} catch (Exception $e) {
$this->manager->fail(
(new Request())
->setUserGuid($user_guid)
->setTimestamp($transaction->getTimestamp())
->setTx($tx)
);
error_log($e);
}
}
public function withdrawFail($log, $transaction)
{
if ($transaction->getContract() !== 'withdraw') {
throw new \Exception("Failed but not a withdrawal");
return;
throw new Exception("Failed but not a withdrawal");
}
$transaction->setFailed(true);
......
......@@ -502,6 +502,7 @@ class Blog extends RepositoryEntity
}
}
$this->nsfw = $array;
$this->markAsDirty('nsfw');
return $this;
}
......
......@@ -60,6 +60,7 @@ class CreateActivity
->setThumbnail($blog->getIconUrl())
->setFromEntity($blog)
->setMature($blog->isMature())
->setNsfw($blog->getNsfw())
->setOwner($owner->export())
->setWireThreshold($blog->getWireThreshold())
->setPaywall($blog->isPaywall());
......
......@@ -138,6 +138,7 @@ class Manager
}
$this->paywallReview->queue($blog);
$this->propagateProperties->from($blog);
}
return $saved;
......
......@@ -102,7 +102,7 @@ class Repository
->setRating($data['rating'])
->setTags($data['tags'])
->setNsfw($data['nsfw'])
->setRejectReason($data['rejection_reason'])
->setRejectedReason($data['rejection_reason'])
->setChecksum($data['checksum']);
$response[] = $boost;
......
<?php
/**
* Captcha Model
*/
namespace Minds\Core\Captcha;
use Minds\Traits\MagicAttributes;
class Captcha
{
use MagicAttributes;
/** @var string */
private $jwtToken;
/** @var string */
private $clientText;
/** @var string */
private $base64Image;
/**
* Export the captcha
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
return [
'jwt_token' => $this->jwtToken,
'base64_image' => $this->base64Image,
];
}
}
<?php
namespace Minds\Core\Captcha;
class ImageGenerator
{
/** @var int */
protected $width = 250;
/** @var int */
protected $height = 100;
/** @var string */
protected $text;
/**
* Set the width
* @param int $width
* @return self
*/
public function setWidth(int $width): self
{
$this->width = $width;
return $this;
}
/**
* Set the height
* @param int $height
* @return self
*/
public function setHeight(int $height): self
{
$this->height = $height;
return $this;
}
/**
* Set the text to output
* @param string $text
* @return self
*/
public function setText(string $text): self
{
$this->text = $text;
return $this;
}
/**
* Outputs the captcha image
* @return string
*/
public function build(): string
{
$image = imagecreatetruecolor($this->width, $this->height);
// Slight grey background
$backgroundColor = imagecolorallocate($image, 240, 240, 240);
// Builds the image background
imagefilledrectangle($image, 0, 0, $this->width, $this->height, $backgroundColor);
// Set the line thickness
imagesetthickness($image, 3);
// Dark grey lines
$lineColor = imagecolorallocate($image, 74, 74, 74);
$numberOfLines = rand(4, 10);
for ($i = 0; $i < $numberOfLines; $i++) {
imagesetthickness($image, rand(1, 3));
imageline($image, 0, rand() % $this->height, $this->width, rand() % $this->height, $lineColor);
}
for ($i = 0; $i< $this->width * 4; $i++) {
$pixelColor = imagecolorallocate($image, rand(0, 255), rand(0, 255), rand(0, 255));
imagesetpixel($image, rand() % $this->width, rand() % $this->height, $pixelColor);
}
$font = __MINDS_ROOT__ . '/Assets/fonts/Roboto-Medium.ttf';
$angle = rand(-6, 6);
$size = rand($this->height * 0.25, $this->height * 0.55);
$x = 10;
$y = ($this->height / 2) + ($size / 2);
$color = imagecolorallocate($image, 64, 64, 64);
// Write the text to the image
imagettftext($image, $size, $angle, $x, $y, $color, $font, $this->text);
ob_start();
imagepng($image);
$imagedata = ob_get_clean();
$base64 = base64_encode($imagedata);
imagedestroy($image);
return "data:image/png;base64,$base64";
}
}
<?php
namespace Minds\Core\Captcha;
use Minds\Common\Jwt;
use Minds\Core\Config;
use Minds\Core\Di\Di;
class Manager
{
/** @var ImageGenerator */
private $imageGenerator;
/** @var JWT */
private $jwt;
/** @var string */
private $secret;
public function __construct($imageGenerator = null, $jwt = null, $config = null)
{
$this->imageGenerator = $imageGenerator ?? new ImageGenerator;
$this->jwt = $jwt ?? new Jwt();
$config = $config ?? Di::_()->get('Config');
$this->secret = $config->get('captcha') ? $config->get('captcha')['jwt_secret'] : 'todo';
$this->jwt->setKey($this->secret);
}
/**
* Verify from client json
* @param string $json
* @return bool
*/
public function verifyFromClientJson(string $json): bool
{
$data = json_decode($json, true);
$captcha = new Captcha();
$captcha->setJwtToken($data['jwtToken'])
->setClientText($data['clientText']);
return $this->verify($captcha);
}
/**
* Verify if a captcha is valid
* @param Captcha $captcha
* @return bool
*/
public function verify(Captcha $captcha): bool
{
$jwtToken = $captcha->getJwtToken();
$decodedJwtToken = $this->jwt->decode($jwtToken);
$salt = $decodedJwtToken['salt'];
$hash = $decodedJwtToken['public_hash'];
// This is what the client has said the captcha image has
$clientText = $captcha->getClientText();
// Now convert this back to our hash
$clientHash = $this->buildCaptchaHash($clientText, $salt);
return $clientHash === $hash;
}
/**
* Output the captcha
* @param string $forcedText
* @return void
*/
public function build(string $forcedText = ''): Captcha
{
$text = $forcedText ?: $this->getRandomText(6);
$now = time();
$expires = $now + 300; // Captcha are good for 5 minutes
$salt = $this->jwt->randomString();
$jwtToken = $this->jwt
->setKey($this->secret)
->encode([
'public_hash' => $this->buildCaptchaHash($text, $salt),
'salt' => $salt,
], $expires, $now);
$image = $this->imageGenerator
->setText($text)
->build();
$captcha = new Captcha();
$captcha->setBase64Image($image)
->setJwtToken($jwtToken);
return $captcha;
}
/**
* Get the random text
* @param int $length
* @return sdtring
*/
protected function getRandomText(int $length): string
{
$chars = array_merge(
range(1, 9),
range('A', 'N'),
range('P', 'Z'), // We don't want O's or 0s
range('a', 'n'),
range('p', 'z')
);
shuffle($chars);
$text="";
for ($i = 0; $i < $length; $i++) {
$text .= $chars[array_rand($chars)];
}
return $text;
}
/**
* Return hash based on text and salt with a secret
* @param string $text
* @param string $salt
* @return string
*/
protected function buildCaptchaHash(string $text, string $salt): string
{
return hash('sha1', $text . $this->secret . $salt);
}
}
<?php
/**
* Captcha Module
*/
namespace Minds\Core\Captcha;
use Minds\Interfaces\ModuleInterface;
class Module implements ModuleInterface
{
/**
* OnInit.
*/
public function onInit()
{
$provider = new Provider();
$provider->register();
}
}
<?php
/**
* Minds Captcha Provider.
*/
namespace Minds\Core\Captcha;
use Minds\Core\Di\Provider as DiProvider;
class Provider extends DiProvider
{
public function register()
{
$this->di->bind('Captcha\Manager', function ($di) {
return new Manager();
}, ['useFactory' => true]);
}
}
......@@ -110,6 +110,7 @@ class ElasticsearchDocumentsDelegate implements ArtifactsDelegateInterface
$query = [
'index' => $this->config->get('elasticsearch')['index'],
'body' => $body,
'conflicts' => 'proceed'
];
$this->elasticsearch->getClient()->updateByQuery($query);
......
......@@ -8,6 +8,7 @@ namespace Minds\Core\Config;
use Minds\Core\Blockchain\Manager as BlockchainManager;
use Minds\Core\Di\Di;
use Minds\Core\Features\Manager as FeaturesManager;
use Minds\Core\I18n\I18n;
use Minds\Core\Navigation\Manager as NavigationManager;
use Minds\Core\Rewards\Contributions\ContributionValues;
......@@ -27,23 +28,31 @@ class Exported
/** @var I18n */
protected $i18n;
/** @var FeaturesManager */
protected $features;
/**
* Exported constructor.
* @param Config $config
* @param ThirdPartyNetworksManager $thirdPartyNetworks
* @param I18n $i18n
* @param BlockchainManager $blockchain
* @param FeaturesManager $features
*/
public function __construct(
$config = null,
$thirdPartyNetworks = null,
$i18n = null,
$blockchain = null
$blockchain = null,
$proDomain = null,
$features = null
) {
$this->config = $config ?: Di::_()->get('Config');
$this->thirdPartyNetworks = $thirdPartyNetworks ?: Di::_()->get('ThirdPartyNetworks\Manager');
$this->i18n = $i18n ?: Di::_()->get('I18n');
$this->blockchain = $blockchain ?: Di::_()->get('Blockchain\Manager');
$this->proDomain = $proDomain ?: Di::_()->get('Pro\Domain');
$this->features = $features ?: Di::_()->get('Features\Manager');
}
/**
......@@ -71,7 +80,7 @@ class Exported
'recaptchaKey' => $this->config->get('google')['recaptcha']['site_key'],
'max_video_length' => $this->config->get('max_video_length'),
'max_video_file_size' => $this->config->get('max_video_file_size'),
'features' => (object) ($this->config->get('features') ?: []),
'features' => (object) ($this->features->export() ?: []),
'blockchain' => (object) $this->blockchain->getPublicSettings(),
'sale' => $this->config->get('blockchain')['sale'],
'last_tos_update' => $this->config->get('last_tos_update') ?: time(),
......@@ -106,6 +115,16 @@ class Exported
$exported['MindsEmbed'] = $embedded_entity ?? null;
}
if ($_GET['__e_cnf_token'] ?? false) {
$exported['from_email_confirmation'] = true;
}
// Pro export
if ($pro = $this->proDomain->lookup($_SERVER['HTTP_HOST'] ?? null)) {
$exported['pro'] = $pro;
}
return $exported;
}
}
<?php
/**
* @author edgebal
*/
namespace Minds\Core;
use Minds\Common\StaticToInstance;
use Minds\Helpers\Counters as CountersHelper;
use ReflectionException;
/**
* Class Counters
* @package Minds\Core
* @method increment($entity, $metric, $value = 1, $client = null)
* @method decrement($entity, $metric, $value = 1, $client = null)
* @method incrementBatch($entities, $metric, $value = 1, $client = null)
* @method get($entity, $metric, $cache = true, $client = null)
* @method clear($entity, $metric, $value = 0, $client = null)
*/
class Counters extends StaticToInstance
{
/**
* Counters constructor.
* @throws ReflectionException
*/
public function __construct()
{
parent::__construct(new CountersHelper());
}
}
......@@ -25,6 +25,9 @@ class DataProvider extends Provider
$this->di->bind('Cache\Apcu', function ($di) {
return new cache\apcu();
}, ['useFactory'=>true]);
$this->di->bind('Cache\PsrWrapper', function ($di) {
return new cache\PsrWrapper();
}, ['useFactory'=>true]);
/**
* Database bindings
*/
......
<?php
/**
* PsrWrapper
*
* @author edgebal
*/
namespace Minds\Core\Data\cache;
use Minds\Core\Di\Di;
use NotImplementedException;
use Psr\SimpleCache\CacheInterface;
class PsrWrapper implements CacheInterface
{
/** @var abstractCacher */
protected $cache;
/**
* PsrWrapper constructor.
* @param abstractCacher $cache
*/
public function __construct(
$cache = null
) {
$this->cache = $cache ?: Di::_()->get('Cache');
}
/**
* @inheritDoc
*/
public function get($key, $default = null)
{
return $this->cache->get($key) ?? $default;
}
/**
* @inheritDoc
*/
public function set($key, $value, $ttl = null)
{
return $this->cache->set($key, $value, $ttl);
}
/**
* @inheritDoc
*/
public function delete($key)
{
$this->cache->destroy($key);
}
/**
* @inheritDoc
* @throws NotImplementedException
*/
public function clear()
{
throw new NotImplementedException();
}
/**
* @inheritDoc
*/
public function getMultiple($keys, $default = null)
{
$values = [];
foreach ($keys as $key) {
$values[$key] = $this->cache->get($key) ?? $default;
}
return $values;
}
/**
* @inheritDoc
*/
public function setMultiple($values, $ttl = null)
{
foreach ($values as $key => $value) {
$this->cache->set($key, $value, $ttl);
}
return true;
}
/**
* @inheritDoc
*/
public function deleteMultiple($keys)
{
foreach ($keys as $key) {
$this->cache->destroy($key);
}
return true;
}
/**
* @inheritDoc
*/
public function has($key)
{
return $this->cache->get($key) !== null;
}
}
<?php
/**
* Ref.
* Holds a forward reference to a provider and a method.
* Used by PSR-7 Router to resolve DI bindings.
*
* @author edgebal
*/
namespace Minds\Core\Di;
use Minds\Traits\MagicAttributes;
/**
* Class Ref
* @package Minds\Core\Di
* @method string getProvider()
* @method Ref setProvider(string $provider)
* @method string getMethod()
* @method Ref setMethod(string $method)
*/
class Ref
{
use MagicAttributes;
/** @var string */
protected $provider;
/** @var string */
protected $method;
/**
* Ref constructor.
* @param string $provider
* @param string $method
*/
public function __construct(string $provider, string $method)
{
$this->setProvider($provider);
$this->setMethod($method);
}
/**
* @param string $provider
* @param string $method
* @return Ref
*/
public static function _(string $provider, string $method): Ref
{
return new static($provider, $method);
}
}
<?php
/**
* Confirmation
*
* @author edgebal
*/
namespace Minds\Core\Email\Campaigns;
use Minds\Core\Di\Di;
use Minds\Core\Email\Confirmation\Url as ConfirmationUrl;
use Minds\Core\Email\Mailer;
use Minds\Core\Email\Message;
use Minds\Core\Email\Template;
class Confirmation extends EmailCampaign
{
/** @var Template */
protected $template;
/** @var Mailer */
protected $mailer;
/** @var ConfirmationUrl */
protected $confirmationUrl;
/**
* Confirmation constructor.
* @param Template $template
* @param Mailer $mailer
* @param ConfirmationUrl $confirmationUrl
*/
public function __construct(
$template = null,
$mailer = null,
$confirmationUrl = null
) {
$this->template = $template ?: new Template();
$this->mailer = $mailer ?: new Mailer();
$this->confirmationUrl = $confirmationUrl ?: Di::_()->get('Email\Confirmation\Url');
$this->campaign = 'global';
$this->topic = 'confirmation';
}
/**
* @return Message
*/
public function build()
{
$tracking = [
'__e_ct_guid' => $this->user->getGUID(),
'campaign' => $this->campaign,
'topic' => $this->topic,
'state' => 'new',
];
$subject = 'Please, confirm your email';
$this->template->setTemplate('default.tpl');
$this->template->setBody('./Templates/confirmation.tpl');
$this->template->set('user', $this->user);
$this->template->set('username', $this->user->username);
$this->template->set('guid', $this->user->guid);
$this->template->set('tracking', http_build_query($tracking));
$this->template->set(
'confirmation_url',
$this->confirmationUrl
->setUser($this->user)
->generate($tracking)
);
$message = new Message();
$message
->setTo($this->user)
->setMessageId(implode(
'-',
[ $this->user->guid, sha1($this->user->getEmail()), sha1($this->campaign . $this->topic . time()) ]
))
->setSubject($subject)
->setHtml($this->template);
return $message;
}
/**
* @return void
*/
public function send()
{
if ($this->user && $this->user->getEmail()) {
// User is still not enabled
$this->mailer->queue(
$this->build(),
true
);
$this->saveCampaignLog();
}
}
}
......@@ -48,8 +48,9 @@ abstract class EmailCampaign
{
if (
!$this->user
|| !$this->user instanceof \Minds\Entities\User
|| !($this->user instanceof \Minds\Entities\User)
|| $this->user->enabled != 'yes'
|| $this->user->banned === 'yes'
) {
return false;
}
......
<table cellspacing="8" cellpadding="8" border="0" width="600" align="center">
<tbody>
<tr>
<td>
<p>
Please take a moment to validate your email address. This helps us prevent spam and ensure users are real.
Click the button below and we'll know it's you.
</p>
</td>
</tr>
<tr>
<td align="center">
<p>
<a href="<?php echo $vars['confirmation_url'] ?>">
<img src="<?php echo $vars['cdn_assets_url'] ?>assets/emails/cta_complete_setup.png" width="142" alt="Complete Setup"/>
</a>
</p>
</td>
</tr>
<tr>
<td>
<p>Also, be sure to download our mobile app using the links below:</p>
</td>
</tr>
<tr align="center">
<td>
<p>
<a href="https://itunes.apple.com/us/app/minds-com/id961771928?ls=1&mt=8" style="text-decoration: none">
<img src="<?php echo $vars['cdn_assets_url']; ?>assets/ext/appstore.png" width="142" alt="Apple App Store"/>
</a>
<a href="<?php echo "{$vars['site_url']}mobile?{$vars['tracking']}"?>" style="text-decoration: none">
<img src="<?php echo $vars['cdn_assets_url']; ?>assets/photos/minds-android-app.png" width="142" alt="Google Play"/>
</a>
</p>
</td>
</tr>
<tr>
<td>
<p>Thank you for being a pioneer of the free and open internet.</p>
</td>
</tr>
</tbody>
</table>
Thank you for a groundbreaking year. We are at the leading edge of a new model for Internet companies focused on transparency, privacy, monetization, decentralized infrastructure and digital democracy.
Please take a moment to read an overview of our major accomplishments from 2019 as well as a preview of the next evolution of Minds. We encourage you to follow the links to see some of the new designs coming next year for a total revamp of the app and user experience.
| |
|:--:|
| [![Read blog](https://cdn-assets.minds.com/emails/read-blog.png){=150x}](https://www.minds.com/minds/blog/2019-and-beyond-1056269757038587904?__e_ct_guid=<?= $vars['guid']?>&campaign=<?= $vars['campaign']?>&topic=<?= $vars['topic'] ?>&validator=<?= $vars['validator'] ?>) |
| |
Thank you for being a part of our journey.
You've received a gift of 1 Minds token! You can spend this token to earn 1,000 extra views on your content with [Boost](https://www.minds.com/boost?__e_ct_guid=<?= $vars['guid']?>&campaign=<?= $vars['campaign']?>&topic=<?= $vars['topic'] ?>&validator=<?= $vars['validator'] ?>) or to tip your favorite content creators with [Wire](https://www.minds.com/wire?__e_ct_guid=<?= $vars['guid']?>&campaign=<?= $vars['campaign']?>&topic=<?= $vars['topic'] ?>&validator=<?= $vars['validator'] ?>).
Please use the button below to claim your gift (note: you will need to open the link in a web browser, the mobile app is not yet supported):
| |
|:--:|
| [![Claim Gift](https://cdn-assets.minds.com/emails/claim-gift.png){=150x}](https://www.minds.com/wallet/tokens/transactions?__e_ct_guid=<?= $vars['guid']?>&campaign=<?= $vars['campaign']?>&topic=<?= $vars['topic'] ?>&validator=<?= $vars['validator'] ?>) |
| |
Our team has been heads down in the lab developing open source technology that is changing the world. As always, our goal is to provide you with a platform that enables the free exchange of ideas, protects your digital rights and fairly compensates you for your contributions to the network.
Introducing Minds Pro (beta), a new revenue model for content creators.
- Get paid for your traffic and referrals
- Launch your own website
- Receive multi-currency tips and subscription payments from fans
- Supports video, images, blogs and more
| |
|:--:|
| [![Upgrade to Pro](https://cdn-assets.minds.com/emails/upgrade-to-pro.png){=150x}](https://www.minds.com/pro?__e_ct_guid=<?= $vars['guid']?>&campaign=<?= $vars['campaign']?>&topic=<?= $vars['topic'] ?>&validator=<?= $vars['validator'] ?>) |
| |
Thank you for your support!
Great news! Your channel is now eligible for a free preview of [Minds Pro](https://minds.com/pro?__e_ct_guid=<?= $vars['guid']?>&campaign=<?= $vars['campaign']?>&topic=<?= $vars['topic'] ?>&validator=<?= $vars['validator'] ?>).
This gives you a chance to test out our new features and decide if the product is right for you. Pro provides you with the tools to launch your own website and monetize your content without fear of unfair algorithms, censorship or demonetization.
| |
|:--:|
| [![Try Pro](https://cdn-assets.minds.com/emails/try-pro.png){=150x}](https://www.minds.com/pro/<?= $vars['user']->username ?>/settings?__e_ct_guid=<?= $vars['guid']?>&campaign=<?= $vars['campaign']?>&topic=<?= $vars['topic'] ?>&validator=<?= $vars['validator'] ?>) |
| |
Specifically, be sure to check out your new [Analytics](https://www.minds.com/analytics/dashboard) console in the right-hand menu once you log in. Please remember that purchasing our [products](https://minds.com/upgrade?__e_ct_guid=<?= $vars['guid']?>&campaign=<?= $vars['campaign']?>&topic=<?= $vars['topic'] ?>&validator=<?= $vars['validator'] ?>) is an invaluable donation to the financial sustainability of our platform and community.
Thank you for your support!
......@@ -82,7 +82,8 @@ class WirePromotions extends EmailCampaign
->setSubject($this->subject)
->setHtml($this->template);
//send email
$this->mailer->send($message);
if ($this->canSend()) {
$this->mailer->send($message);
}
}
}
<?php
/**
* Manager
*
* @author edgebal
*/
namespace Minds\Core\Email\Confirmation;
use Exception;
use Minds\Common\Jwt;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Events\EventsDispatcher;
use Minds\Core\Queue\Client as QueueClientFactory;
use Minds\Core\Queue\Interfaces\QueueClient;
use Minds\Entities\User;
use Minds\Entities\UserFactory;
class Manager
{
/** @var Config */
protected $config;
/** @var Jwt */
protected $jwt;
/** @var QueueClient */
protected $queue;
/** @var UserFactory */
protected $userFactory;
/** @var EventsDispatcher */
protected $eventsDispatcher;
/** @var User */
protected $user;
/**
* Manager constructor.
* @param Config $config
* @param Jwt $jwt
* @param QueueClient $queue
* @param UserFactory $userFactory
* @param EventsDispatcher $eventsDispatcher
* @throws Exception
*/
public function __construct(
$config = null,
$jwt = null,
$queue = null,
$userFactory = null,
$eventsDispatcher = null
) {
$this->config = $config ?: Di::_()->get('Config');
$this->jwt = $jwt ?: new Jwt();
$this->queue = $queue ?: QueueClientFactory::build();
$this->userFactory = $userFactory ?: new UserFactory();
$this->eventsDispatcher = $eventsDispatcher ?: Di::_()->get('EventsDispatcher');
}
/**
* @param User $user
* @return Manager
*/
public function setUser(User $user): Manager
{
$this->user = $user;
return $this;
}
/**
* @throws Exception
*/
public function sendEmail(): void
{
if (!$this->user) {
throw new Exception('User not set');
}
if ($this->user->isEmailConfirmed()) {
throw new Exception('User email was already confirmed');
}
$config = $this->config->get('email_confirmation');
$now = time();
$expires = $now + $config['expiration'];
$token = $this->jwt
->setKey($config['signing_key'])
->encode([
'user_guid' => (string) $this->user->guid,
'code' => $this->jwt->randomString(),
], $expires, $now);
$this->user
->setEmailConfirmationToken($token)
->save();
$this->eventsDispatcher->trigger('confirmation_email', 'all', [
'user_guid' => (string) $this->user->guid,
'cache' => false,
]);
}
/**
* @return bool
* @throws Exception
*/
public function reset(): bool
{
if (!$this->user) {
throw new Exception('User not set');
}
$this->user
->setEmailConfirmationToken('')
->setEmailConfirmedAt(0);
return (bool) $this->user
->save();
}
/**
* @param string $jwt
* @return bool
* @throws Exception
*/
public function confirm(string $jwt): bool
{
$config = $this->config->get('email_confirmation');
if ($this->user) {
throw new Exception('Confirmation user is inferred from JWT');
}
$confirmation = $this->jwt
->setKey($config['signing_key'])
->decode($jwt); // Should throw if expired
if (
!$confirmation ||
!$confirmation['user_guid'] ||
!$confirmation['code']
) {
throw new Exception('Invalid JWT');
}
$user = $this->userFactory->build($confirmation['user_guid'], false);
if (!$user || !$user->guid) {
throw new Exception('Invalid user');
} elseif ($user->isEmailConfirmed()) {
throw new Exception('User email was already confirmed');
}
$data = $this->jwt
->setKey($config['signing_key'])
->decode($user->getEmailConfirmationToken());
if (
$data['user_guid'] !== $confirmation['user_guid'] ||
$data['code'] !== $confirmation['code']
) {
throw new Exception('Invalid confirmation token data');
}
$user
->setEmailConfirmationToken('')
->setEmailConfirmedAt(time())
->save();
$this->queue
->setQueue('WelcomeEmail')
->send([
'user_guid' => (string) $user->guid,
]);
return true;
}
}
<?php
/**
* Url
*
* @author edgebal
*/
namespace Minds\Core\Email\Confirmation;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Entities\User;
class Url
{
/** @var string */
const EMAIL_CONFIRMATION_PATH = '/email-confirmation';
/** @var Config */
protected $config;
/** @var User */
protected $user;
/**
* ConfirmationUrlDelegate constructor.
* @param Config $config
*/
public function __construct(
$config = null
) {
$this->config = $config ?: Di::_()->get('Config');
}
/**
* @param User $user
* @return Url
*/
public function setUser(User $user): Url
{
$this->user = $user;
return $this;
}
/**
* @param array $params
* @return string
*/
public function generate(array $params = []): string
{
return sprintf(
'%s%s?%s',
rtrim($this->config->get('site_url'), '/'),
static::EMAIL_CONFIRMATION_PATH,
http_build_query(array_merge($params, [
'__e_cnf_token' => $this->user->getEmailConfirmationToken(),
]))
);
}
}
<?php
/**
* ConfirmationSender
*
* @author edgebal
*/
namespace Minds\Core\Email\Delegates;
use Minds\Core\Email\Campaigns\Confirmation;
use Minds\Entities\User;
use Minds\Interfaces\SenderInterface;
class ConfirmationSender implements SenderInterface
{
/**
* @param User $user
*/
public function send(User $user)
{
(new Confirmation())
->setUser($user)
->send();
}
}
......@@ -52,11 +52,15 @@ class Events
Dispatcher::register('welcome_email', 'all', function ($opts) {
$this->sendCampaign(new Delegates\WelcomeSender(), $opts->getParameters());
});
Dispatcher::register('confirmation_email', 'all', function ($opts) {
$this->sendCampaign(new Delegates\ConfirmationSender(), $opts->getParameters());
});
}
private function sendCampaign(SenderInterface $sender, $params)
{
$user = new User($params['user_guid']);
$user = new User($params['user_guid'], $params['cache'] ?? true);
$sender->send($user);
}
}
......@@ -37,7 +37,7 @@ class Mailer
$this->mailer->SMTPAuth = true;
$this->mailer->Username = Core\Config::_()->email['smtp']['username'];
$this->mailer->Password = Core\Config::_()->email['smtp']['password'];
$this->mailer->SMTPSecure = "ssl";
$this->mailer->SMTPSecure = Core\Config::_()->email['smtp']['smtp_secure'] ?? "ssl";
$this->mailer->Port = Core\Config::_()->email['smtp']['port'];
}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.