...
 
Commits (32)
......@@ -80,7 +80,7 @@ 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 \
......
......@@ -181,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();
......
......@@ -339,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
......
......@@ -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()}");
}
}
}
......@@ -31,4 +31,31 @@ class Transcode extends Cli\Controller implements Interfaces\CliControllerInterf
$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 ]);
}
}
......@@ -337,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);
......
......@@ -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
/**
* 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([]);
}
}
......@@ -17,7 +17,8 @@ class feeds implements Interfaces\Api
'12h' => '7d',
'24h' => '7d',
'7d' => '30d',
'30d' => '1y'
'30d' => '1y',
'1y' => 'all'
];
/**
......@@ -212,6 +213,7 @@ class feeds implements Interfaces\Api
!$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;
......@@ -221,6 +223,7 @@ class feeds implements Interfaces\Api
$from = $now - $periodsInSecs[$period];
$opts['from_timestamp'] = $from * 1000;
$opts['period'] = static::PERIOD_FALLBACK[$period];
$opts['limit'] = $limit - $entities->count();
if (!$fallbackAt) {
$fallbackAt = $from;
......
......@@ -28,11 +28,13 @@ class channel implements Interfaces\Api
*/
public function get($pages)
{
$currentUser = Session::getLoggedinUser();
$channel = new User(strtolower($pages[0]));
$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'
......
......@@ -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([
......@@ -61,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';
......@@ -125,10 +123,10 @@ 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' => $algorithm,
'period' => '7d',
......@@ -162,7 +160,11 @@ class content implements Interfaces\Api
try {
$result = $this->getData($entities, $opts, $asActivities, $sync);
if ($opts['algorithm'] !== 'latest' && $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);
}
......@@ -190,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');
/** @var Core\Feeds\Elastic\Manager $manager */
$manager = Di::_()->get('Feeds\Elastic\Manager');
$result = $manager->getList($opts);
$result = $manager
->setUserGuid($opts['container_guid'])
->fetch();
if (!$sync) {
// Remove all unlisted content, if ES document is not in sync, it'll
// also remove pending activities
$result = $result->filter([$entities, 'filter']);
break;
if ($asActivities) {
// Cast to ephemeral Activity entities, if another type
$result = $result->map([$entities, 'cast']);
}
default:
/** @var Core\Feeds\Elastic\Manager $manager */
$manager = Di::_()->get('Feeds\Elastic\Manager');
$result = $manager->getList($opts);
if (!$sync) {
// Remove all unlisted content, if ES document is not in sync, it'll
// also remove pending activities
$result = $result->filter([$entities, 'filter']);
if ($asActivities) {
// Cast to ephemeral Activity entities, if another type
$result = $result->map([$entities, 'cast']);
}
}
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'])) {
......
......@@ -104,7 +104,7 @@ class account implements Interfaces\Api
Factory::isLoggedIn();
$response = [];
$vars = Core\Router::getPutVars();
$vars = Core\Router\PrePsr7\Router::getPutVars();
$user = Core\Session::getLoggedInUser();
......
......@@ -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();
......
......@@ -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' => [
......
......@@ -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;
......
......@@ -110,6 +110,7 @@ class ElasticsearchDocumentsDelegate implements ArtifactsDelegateInterface
$query = [
'index' => $this->config->get('elasticsearch')['index'],
'body' => $body,
'conflicts' => 'proceed'
];
$this->elasticsearch->getClient()->updateByQuery($query);
......
......@@ -106,6 +106,10 @@ class Exported
$exported['MindsEmbed'] = $embedded_entity ?? null;
}
if ($_GET['__e_cnf_token'] ?? false) {
$exported['from_email_confirmation'] = true;
}
return $exported;
}
}
<?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 = 'Confirm your Minds email (Action required)';
$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>
Thanks,<br>
The Minds Team
</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.
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'];
}
......
......@@ -41,5 +41,13 @@ class Provider extends DiProvider
$this->di->bind('Email\CampaignLogs\Repository', function ($di) {
return new CampaignLogs\Repository();
}, ['useFactory' => true]);
$this->di->bind('Email\Confirmation', function ($di) {
return new Confirmation\Manager();
}, ['useFactory' => true]);
$this->di->bind('Email\Confirmation\Url', function ($di) {
return new Confirmation\Url();
}, ['useFactory' => true]);
}
}
......@@ -2,13 +2,29 @@
namespace Minds\Core\Email;
use Exception;
use Minds\Core\Analytics\Metrics\Event;
use Minds\Core\Di\Di;
use Minds\Core\Email\Confirmation\Manager as ConfirmationManager;
use Minds\Core\Session;
class RouterHooks
{
public function __construct($event = null)
/** @var Event */
protected $event;
/** @var ConfirmationManager */
protected $confirmationManager;
/**
* RouterHooks constructor.
* @param Event $event
* @param ConfirmationManager $confirmationManager
*/
public function __construct($event = null, $confirmationManager = null)
{
$this->event = $event ?: new Event();
$this->confirmationManager = $confirmationManager ?: Di::_()->get('Email\Confirmation');
}
public function withRouterRequest($request)
......@@ -18,6 +34,17 @@ class RouterHooks
$action = 'email:clicks';
if (strpos($path, '/emails/unsubscribe') !== false) {
$action = 'email:unsubscribe';
} elseif (isset($queryParams['__e_cnf_token'])) {
try {
$this->confirmationManager
->confirm($queryParams['__e_cnf_token']);
} catch (Exception $e) {
// Do not continue processing.
// TODO: Log?
return;
}
$action = 'email:confirm';
}
$platform = isset($queryParams['cb']) ? 'mobile' : 'browser';
if (isset($queryParams['platform'])) {
......
......@@ -55,17 +55,22 @@ class Register
if ($params['user']->captcha_failed) {
return false;
}
//send welcome email
try {
/** @var Core\Email\Confirmation\Manager $emailConfirmation */
$emailConfirmation = Di::_()->get('Email\Confirmation');
$emailConfirmation
->setUser($params['user'])
->sendEmail();
} catch (\Exception $e) {
error_log((string) $e);
}
try {
Core\Queue\Client::build()->setQueue('Registered')
->send([
'user_guid' => $params['user']->guid,
'user_guid' => (string) $params['user']->guid,
]);
//Delay by 15 minutes (aws max) so the user has time to complete their profile
Core\Queue\Client::build()->setQueue('WelcomeEmail')
->send([
'user_guid' => $params['user']->guid,
], 900);
} catch (\Exception $e) {
}
});
......
......@@ -51,7 +51,7 @@ class Manager
$features = $this->config->get('features') ?: [];
if (!isset($features[$feature])) {
error_log("[Features\Manager] Feature '{$feature}' is not declared. Assuming false.");
// error_log("[Features\Manager] Feature '{$feature}' is not declared. Assuming false.");
return false;
}
......
......@@ -6,14 +6,21 @@
namespace Minds\Core\Feeds\Activity;
use Zend\Diactoros\ServerRequest;
class Manager
{
public function add()
public function add(ServerRequest $request)
{
throw new \NotImplementedException();
}
public function update(ServerRequest $request)
{
throw new \NotImplementedException();
}
public function update()
public function delete(ServerRequest $request)
{
throw new \NotImplementedException();
}
......
......@@ -17,6 +17,7 @@ class Repository
'7d' => 604800,
'30d' => 2592000,
'1y' => 31536000,
'all' => -1,
];
/** @var ElasticsearchClient */
......@@ -265,7 +266,7 @@ class Repository
$timestampUpperBounds = []; // LTE
$timestampLowerBounds = []; // GT
if ($algorithm->isTimestampConstrain()) {
if ($algorithm->isTimestampConstrain() && static::PERIODS[$opts['period']] > -1) {
$timestampLowerBounds[] = (time() - static::PERIODS[$opts['period']]) * 1000;
}
......@@ -323,23 +324,25 @@ class Repository
];
}
} elseif ($opts['hashtags']) {
if ($opts['filter_hashtags'] || $algorithm instanceof SortingAlgorithms\Chronological) {
if (!isset($body['query']['function_score']['query']['bool']['must'])) {
$body['query']['function_score']['query']['bool']['must'] = [];
}
$body['query']['function_score']['query']['bool']['must'][] = [
'terms' => [
'tags' => $opts['hashtags'],
],
];
} else {
$body['query']['function_score']['query']['bool']['must'][] = [
'terms' => [
'tags' => $opts['hashtags'],
],
];
if (!isset($body['query']['function_score']['query']['bool']['should'])) {
$body['query']['function_score']['query']['bool']['should'] = [];
}
$body['query']['function_score']['query']['bool']['should'][] = [
'terms' => [
'tags' => $opts['hashtags'],
'boost' => 1, // hashtags are 10x more valuable then non-hashtags
],
];
$body['query']['function_score']['query']['bool']['should'][] = [
'multi_match' => [
'query' => implode(' ', $opts['hashtags']),
'fields' => ['title', 'message', 'description'],
'operator' => 'or',
'boost' => 0.1
],
];
$body['query']['function_score']['query']['bool']['minimum_should_match'] = 1;
}
if ($opts['exclude']) {
......
......@@ -12,6 +12,10 @@ class FeedsProvider extends Provider
return new Elastic\Manager();
});
$this->di->bind('Feeds\Activity\Manager', function ($di) {
return new Activity\Manager();
});
$this->di->bind('Feeds\Firehose\Manager', function ($di) {
return new Firehose\Manager();
});
......
<?php
/**
* Module
* @author edgebal
*/
namespace Minds\Core\Feeds;
use Minds\Interfaces\ModuleInterface;
class Module implements ModuleInterface
{
/**
* Executed onInit
* @return void
*/
public function onInit(): void
{
(new FeedsProvider())->register();
(new Routes())->register();
}
}
<?php
/**
* Routes
* @author edgebal
*/
namespace Minds\Core\Feeds;
use Minds\Core\Di\Ref;
use Minds\Core\Router\Middleware\LoggedInMiddleware;
use Minds\Core\Router\ModuleRoutes;
use Minds\Core\Router\Route;
class Routes extends ModuleRoutes
{
/**
* Registers all module routes
*/
public function register(): void
{
$this->route
->withPrefix('api/v3/newsfeed')
->withMiddleware([
LoggedInMiddleware::class,
])
->do(function (Route $route) {
$route->post(
'',
Ref::_('Feeds\Activity\Manager', 'add')
);
$route->post(
':guid',
Ref::_('Feeds\Activity\Manager', 'update')
);
$route->delete(
':guid',
Ref::_('Feeds\Activity\Manager', 'delete')
);
});
}
}
......@@ -20,5 +20,9 @@ class GroupsProvider extends Provider
$this->di->bind('Groups\Feeds', function ($di) {
return new Feeds();
}, [ 'useFactory'=> false ]);
$this->di->bind('Groups\Ownership', function ($di) {
return new Ownership();
}, [ 'useFactory'=> true ]);
}
}
......@@ -634,6 +634,22 @@ class Membership
$user->context('');
}
public function getGroupGuidsByMember($opts = [])
{
$opts = array_merge([
'limit' => 500
], $opts);
// Grab all groups we are a member of
$this->relDB
->setGuid((string) $opts['user_guid']);
return $this->relDB->get('member', [
'limit' => $opts['limit'],
'inverse' => false,
]);
}
public function getGroupsByMember($opts = [])
{
$opts = array_merge([
......@@ -649,11 +665,9 @@ class Membership
'limit' => 1000,
])->toArray();
// Grab all groups we are a member of
$this->relDB->setGuid($opts['user_guid']);
$guids = $this->relDB->get('member', [
$guids = $this->getGroupGuidsByMember([
'user_guid' => $opts['user_guid'],
'limit' => 500,
'inverse' => false,
]);
// Populate all groups to markers
......
<?php
/**
* Ownership
* @author edgebal
*/
namespace Minds\Core\Groups;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Di\Di;
use Minds\Core\EntitiesBuilder;
use Minds\Entities\Group;
use Minds\Entities\User;
class Ownership
{
/** @var Membership */
protected $manager;
/** @var EntitiesBuilder */
protected $entitiesBuilder;
/** @var string */
protected $userGuid;
/**
* Ownership constructor.
* @param Membership $manager
* @param EntitiesBuilder $entitiesBuilder
*/
public function __construct(
$manager = null,
$entitiesBuilder = null
) {
$this->manager = $manager ?: new Membership();
$this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder');
}
/**
* @param int|string $userGuid
* @return Ownership
*/
public function setUserGuid($userGuid): Ownership
{
$this->userGuid = (string) $userGuid;
return $this;
}
/**
* @param array $opts
* @return Response
* @throws Exception
*/
public function fetch(array $opts = [])
{
$opts = array_merge([
'cap' => 500,
'offset' => 0,
'limit' => 12,
], $opts);
$guids = $this->manager->getGroupGuidsByMember([
'user_guid' => $this->userGuid,
'limit' => $opts['cap']
]);
$offset = $opts['offset'] ?: 0;
$limit = $opts['limit'];
$guids = array_slice($guids, $offset, $limit ?: null);
if (!$guids) {
return new Response();
}
$user = new User();
$user->guid = $this->userGuid;
$response = (new Response(
$this->entitiesBuilder->get(['guids' => $guids])
))->filter(function ($group) use ($user) {
/** @var Group $group */
return $group && $group->isPublic() && $group->isOwner($user);
})->sort(function ($a, $b) {
/** @var Group $a */
/** @var Group $b */
return $b->getMembersCount() <=> $a->getMembersCount();
});
$response
->setPagingToken($offset + $limit);
return $response;
}
}
......@@ -45,8 +45,8 @@ class Repository
'must' => [
[
'range' => [
'votes:up:24h:synced' => [
'gte' => $opts['from'],
'@timestamp' => [
'gte' => $opts['from'] * 1000,
],
],
],
......@@ -81,7 +81,7 @@ class Repository
'aggs' => [
'counts' => [
'max' => [
'field' => 'votes:up:24h',
'field' => 'votes:up',
],
],
'owners' => [
......
......@@ -69,8 +69,13 @@ class Manager
*/
public function getSources(Video $video): array
{
$guid = $video->getGuid();
if (($legacyGuid = $video->get('cinemr_guid')) && $legacyGuid != $guid) {
$guid = $legacyGuid;
}
$transcodes = $this->transcoderManager->getList([
'guid' => $video->getGuid(),
'guid' => $guid,
'legacyPolyfill' => true,
]);
$sources = [];
......
......@@ -201,7 +201,7 @@ class Manager
public function transcode(Transcode $transcode): void
{
// Update the background so everyone knows this is inprogress
$transcode->setStatus('transcoding');
$transcode->setStatus(TranscodeStates::TRANSCODING);
$this->update($transcode, [ 'status' ]);
// Perform the transcode
......@@ -215,11 +215,12 @@ class Manager
throw new TranscodeExecutors\FailedTranscodeException();
}
$transcode->setProgress(100); // If completed should be assumed 100%
$transcode->setStatus('completed');
$transcode->setStatus(TranscodeStates::COMPLETED);
} catch (TranscodeExecutors\FailedTranscodeException $e) {
$transcode->setStatus('failed');
$transcode->setStatus(TranscodeStates::FAILED);
$transcode->setFailureReason($e->getMessage());
} finally {
$this->update($transcode, [ 'progress', 'status' ]);
$this->update($transcode, [ 'progress', 'status', 'failureReason' ]);
}
$this->notificationDelegate->onTranscodeCompleted($transcode);
......
......@@ -57,7 +57,7 @@ class Repository
if ($opts['profileId']) {
$where[] = "profile_id = ?";
$values[] = $opts['profile_id'];
$values[] = $opts['profileId'];
}
if ($opts['status']) {
......@@ -185,6 +185,9 @@ class Repository
case 'bytes':
$set['bytes'] = (int) $transcode->getBytes();
break;
case 'failureReason':
$set['failure_reason'] = $transcode->getFailureReason();
break;
}
}
......
......@@ -23,6 +23,8 @@ use Minds\Traits\MagicAttributes;
* @method int getLengthSecs()
* @method Transcode setBytes(int $bytes)
* @method int getBytes()
* @method Transcode setFailureReason(string $reason)
* @method string getFailureReason()
*/
class Transcode
{
......@@ -30,10 +32,10 @@ class Transcode
/** @var string */
const TRANSCODE_STATES = [
'created',
'transcoding',
'failed',
'completed',
TranscodeStates::CREATED,
TranscodeStates::TRANSCODING,
TranscodeStates::FAILED,
TranscodeStates::COMPLETED,
];
/** @var string */
......@@ -60,6 +62,9 @@ class Transcode
/** @var int */
private $bytes;
/** @var string */
private $failureReason;
/**
* @param Video $video
* @return self
......
......@@ -15,6 +15,7 @@ use Minds\Entities\Video;
use Minds\Core\Di\Di;
use Minds\Core\Media\TranscodingStatus;
use Minds\Core\Media\Video\Transcoder\Transcode;
use Minds\Core\Media\Video\Transcoder\TranscodeStates;
use Minds\Core\Media\Video\Transcoder\TranscodeStorage\TranscodeStorageInterface;
use Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
......@@ -149,10 +150,11 @@ class FFMpegExecutor implements TranscodeExecutorInterface
// Completed!
// $this->logger->info("Completed: $path ({$transcode->getGuid()})");
$transcode->setStatus('completed');
$transcode->setStatus(TranscodeStates::COMPLETED);
} catch (\Exception $e) {
error_log("FAILED: {$transcode->getGuid()} {$e->getMessage()}");
// $this->logger->out("Failed {$e->getMessage()}");
$transcode->setStatus('failed');
$transcode->setStatus(TranscodeStates::FAILED);
// TODO: Should we also save the failure reason in the db?
// Throw a new 'failed' exception
throw new FailedTranscodeException($e->getMessage());
......@@ -181,7 +183,7 @@ class FFMpegExecutor implements TranscodeExecutorInterface
// Create thumbnails
$length = round((int) $this->ffprobe->format($sourcePath)->get('duration'));
$secs = [0, 1, round($length / 2), $length - 1, $length];
$secs = [0, 1, round($length / 2), $length - 1];
foreach ($secs as $sec) {
$frame = $video->frame(\FFMpeg\Coordinate\TimeCode::fromSeconds($sec));
$pad = str_pad($sec, 5, '0', STR_PAD_LEFT);
......@@ -198,9 +200,10 @@ class FFMpegExecutor implements TranscodeExecutorInterface
@unlink($path);
}
$transcode->setProgress(100);
$transcode->setStatus('completed');
$transcode->setStatus(TranscodeStates::COMPLETED);
} catch (\Exception $e) {
$transcode->setStatus('failed');
error_log("FAILED: {$transcode->getGuid()} {$e->getMessage()}");
$transcode->setStatus(TranscodeStates::FAILED);
// TODO: Should we also save the failure reason in the db?
// Throw a new 'failed' exception
throw new FailedTranscodeException($e->getMessage());
......
......@@ -101,6 +101,6 @@ class S3Storage implements TranscodeStorageInterface
]);
$s3Contents = $awsResult['Contents'];
return array_column($s3Contents, 'Key');
return array_column($s3Contents, 'Key') ?: [];
}
}
......@@ -4,6 +4,7 @@ namespace Minds\Core;
use Minds\Core\Di\Di;
use Minds\Core\Events\Dispatcher;
use Minds\Interfaces\ModuleInterface;
/**
* Core Minds Engine.
......@@ -28,6 +29,7 @@ class Minds extends base
Referrals\Module::class,
Reports\Module::class,
VideoChat\Module::class,
Feeds\Module::class,
Front\Module::class,
];
......@@ -60,6 +62,7 @@ class Minds extends base
/*
* Initialise the modules
*/
/** @var ModuleInterface $module */
foreach ($modules as $module) {
$module->onInit();
}
......@@ -111,7 +114,6 @@ class Minds extends base
(new Plus\PlusProvider())->register();
(new Pro\ProProvider())->register();
(new Hashtags\HashtagsProvider())->register();
(new Feeds\FeedsProvider())->register();
(new Analytics\AnalyticsProvider())->register();
(new Channels\ChannelsProvider())->register();
(new Blogs\BlogsProvider())->register();
......
......@@ -12,8 +12,12 @@ use DateTime;
class Manager
{
/** @var int */
const VIEWS_RPM_CENTS = 100; // $1 USD
/** @var int */
const REFERRAL_CENTS = 10; // $0.10
/** @var Repository */
private $repository;
......@@ -60,7 +64,18 @@ class Manager
$opts = array_merge([
'from' => strtotime('midnight'),
], $opts);
yield from $this->issuePageviewDeposits($opts);
yield from $this->issueReferralDeposits($opts);
}
/**
* Issuse the pageview deposits
* @param array
* @return iterable
*/
protected function issuePageviewDeposits(array $opts): iterable
{
$users = [];
foreach ($this->entityCentricManager->getListAggregatedByOwner([
......@@ -92,4 +107,37 @@ class Manager
yield $deposit;
}
}
/**
* Issue the referral deposits
* @param array
* @return iterable
*/
protected function issueReferralDeposits(array $opts): iterable
{
foreach ($this->entityCentricManager->getListAggregatedByOwner([
'fields' => [ 'referral::active' ],
'from' => strtotime('-7 days', $opts['from']),
]) as $ownerSum) {
$count = $ownerSum['referral::active']['value'];
$amountCents = $count * static::REFERRAL_CENTS;
// Is this user in the pro program?
$owner = $this->entitiesBuilder->single($ownerSum['key']);
if (!$owner || !$owner->isPro()) {
continue;
}
$deposit = new EarningsDeposit();
$deposit->setTimestamp($opts['from'])
->setUserGuid($ownerSum['key'])
->setAmountCents($amountCents)
->setItem("referrals");
$this->repository->add($deposit);
yield $deposit;
}
}
}
......@@ -157,6 +157,13 @@ class Manager
'user_guid' => $this->user->guid,
])->first();
// If requested by an inactive user, this is preview mode
if (!$settings && !$this->isActive()) {
$settings = new Settings();
$settings->setUserGuid($this->user->guid);
$settings->setTitle($this->user->name ?: $this->user->username);
}
if (!$settings) {
return null;
}
......@@ -333,8 +340,11 @@ class Manager
$settings->setTimeUpdated(time());
$this->setupRoutingDelegate
->onUpdate($settings);
// Only update routing if we are active
if ($this->isActive()) {
$this->setupRoutingDelegate
->onUpdate($settings);
}
return $this->repository->update($settings);
}
......
......@@ -1543,4 +1543,6 @@ CREATE TABLE minds.video_transcodes (
length_secs int,
bytes int,
PRIMARY KEY (guid, profile_id)
);
\ No newline at end of file
);
ALTER TABLE minds.video_transcodes ADD failure_reason text;
\ No newline at end of file
......@@ -116,6 +116,6 @@ class ChannelDeferredOps implements Interfaces\QueueRunner
default:
echo "ERROR! Invalid type {$type} passed\n\n";
}
});
}, [ 'max_messages' => 1 ]);
}
}
......@@ -242,7 +242,9 @@ class Repository
->setAppeal(isset($row['appeal_note']) ? true : false)
->setAppealNote(isset($row['appeal_note']) ? (string) $row['appeal_note'] : '')
->setReports(
isset($row['reports']) ?
$this->buildReports($row['reports']->values())
: null
)
->setInitialJuryDecisions(
isset($row['initial_jury']) ?
......
This diff is collapsed.
<?php declare(strict_types=1);
/**
* Dispatcher
* @author edgebal
*/
namespace Minds\Core\Router;
use Minds\Core\Router\Middleware\Kernel\EmptyResponseMiddleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class Dispatcher implements RequestHandlerInterface
{
/** @var MiddlewareInterface */
protected $emptyResponseMiddleware;
/**
* Dispatcher constructor.
* @param MiddlewareInterface $emptyResponseMiddleware
*/
public function __construct(
$emptyResponseMiddleware = null
) {
$this->emptyResponseMiddleware = $emptyResponseMiddleware ?: new EmptyResponseMiddleware();
}
/** @var MiddlewareInterface[] */
protected $middleware = [];
/**
* @param MiddlewareInterface $middleware
* @return $this
*/
public function pipe(MiddlewareInterface $middleware): self
{
$this->middleware[] = $middleware;
return $this;
}
/**
* Handles a request and produces a response.
*
* May call other collaborating code to generate the response.
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
if (count($this->middleware) === 0) {
return $this->emptyResponseMiddleware->process($request, $this);
}
$middleware = array_shift($this->middleware);
return $middleware->process($request, $this);
}
}
<?php declare(strict_types=1);
/**
* ForbiddenException
* @author edgebal
*/
namespace Minds\Core\Router\Exceptions;
use Exception;
class ForbiddenException extends Exception
{
}
<?php
/**
* UnauthorizedException
* @author edgebal
*/
namespace Minds\Core\Router\Exceptions;
use Exception;
class UnauthorizedException extends Exception
{
}
<?php
/**
* Manager
* @author edgebal
*/
namespace Minds\Core\Router;
use Minds\Core\Router\Middleware;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequest;
class Manager
{
/** @var Middleware\RouterMiddleware[] */
protected $middleware;
/**
* Manager constructor.
* @param Middleware\RouterMiddleware[] $middleware
*/
public function __construct(
$middleware = null
) {
$this->middleware = $middleware ?: [
new Middleware\SEOMiddleware(),
new Middleware\ProMiddleware(), // this needs to always be the last element in this array
];
}
/**
* @param ServerRequest $request
* @param JsonResponse $response
* @return bool|null
*/
public function handle(ServerRequest &$request, JsonResponse &$response): ?bool
{
$result = null;
foreach ($this->middleware as $middleware) {
$result = $middleware->onRequest($request, $response);
if ($result === false) {
break;
}
}
return $result;
}
}
<?php declare(strict_types=1);
/**
* AdminMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware;
use Minds\Core\Router\Exceptions\ForbiddenException;
use Minds\Core\Router\Exceptions\UnauthorizedException;
use Minds\Core\Security\XSRF;
use Minds\Entities\User;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class AdminMiddleware implements MiddlewareInterface
{
/** @var string */
protected $attributeName = '_user';
/** @var callable */
private $xsrfValidateRequest;
public function __construct(
$xsrfValidateRequest = null
) {
$this->xsrfValidateRequest = $xsrfValidateRequest ?: [XSRF::class, 'validateRequest'];
}
/**
* @param string $attributeName
* @return AdminMiddleware
*/
public function setAttributeName(string $attributeName): AdminMiddleware
{
$this->attributeName = $attributeName;
return $this;
}
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
* @throws ForbiddenException
* @throws UnauthorizedException
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (
!$request->getAttribute($this->attributeName) ||
!call_user_func($this->xsrfValidateRequest)
) {
throw new UnauthorizedException();
}
/** @var User $currentUser */
$currentUser = $request->getAttribute($this->attributeName);
if (!$currentUser->isAdmin()) {
throw new ForbiddenException();
}
return $handler
->handle($request);
}
}
<?php declare(strict_types=1);
/**
* ContentNegotiationMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware\Kernel;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class ContentNegotiationMiddleware implements MiddlewareInterface
{
/** @var string[] */
const JSON_MIME_TYPES = ['application/json', 'text/json', 'application/x-json'];
/** @var string[] */
const HTML_MIME_TYPES = ['text/html', 'application/xhtml+xml'];
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$accept = array_map([$this, '_normalizeAcceptEntries'], explode(',', implode(',', $request->getHeader('Accept'))));
if (array_intersect($accept, static::JSON_MIME_TYPES)) {
$request = $request
->withAttribute('accept', 'json');
} elseif (array_intersect($accept, static::HTML_MIME_TYPES)) {
$request = $request
->withAttribute('accept', 'html');
}
return $handler
->handle($request);
}
/**
* @param $value
* @return mixed
*/
protected function _normalizeAcceptEntries(string $value): string
{
$fragments = explode(';', $value);
return $fragments[0];
}
}
<?php declare(strict_types=1);
/**
* CorsMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware\Kernel;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response\EmptyResponse;
class CorsMiddleware implements MiddlewareInterface
{
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($request->getMethod() === 'OPTIONS') {
return new EmptyResponse(204, [
'Access-Control-Allow-Origin' => $request->getHeaderLine('Origin'),
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Max-Age' => '86400',
'Access-Control-Allow-Methods' => implode(',', [
'GET',
'POST',
'PUT',
'DELETE',
'OPTIONS',
]),
'Access-Control-Allow-Headers' => implode(',', [
'Accept',
'Authorization',
'Cache-Control',
'Content-Type',
'DNT',
'If-Modified-Since',
'Keep-Alive',
'Origin',
'User-Agent',
'X-Mx-ReqToken',
'X-Requested-With',
'X-No-Cache',
]),
]);
}
return $handler
->handle($request);
}
}
<?php declare(strict_types=1);
/**
* EmptyResponseMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware\Kernel;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\JsonResponse;
class EmptyResponseMiddleware implements MiddlewareInterface
{
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$message = 'Endpoint Not Found';
$status = 404;
switch ($request->getAttribute('accept')) {
case 'html':
return new HtmlResponse(sprintf('<h1>%s</h1>', $message), $status);
case 'json':
default:
return new JsonResponse([
'status' => 'error',
'message' => $message,
], $status);
}
}
}
<?php declare(strict_types=1);
/**
* ErrorHandlerMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware\Kernel;
use Exception;
use Minds\Core\Router\Exceptions\ForbiddenException;
use Minds\Core\Router\Exceptions\UnauthorizedException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\JsonResponse;
use function Sentry\captureException;
class ErrorHandlerMiddleware implements MiddlewareInterface
{
/** @var bool */
protected $sentryEnabled = true;
/**
* @param bool $sentryEnabled
* @return ErrorHandlerMiddleware
*/
public function setSentryEnabled(bool $sentryEnabled): ErrorHandlerMiddleware
{
$this->sentryEnabled = $sentryEnabled;
return $this;
}
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$message = 'Internal Server Error';
$status = 500;
try {
return $handler
->handle($request);
} catch (UnauthorizedException $e) {
$message = 'Unauthorized';
$status = 401;
} catch (ForbiddenException $e) {
$message = 'Forbidden';
$status = 403;
} catch (Exception $e) {
// Log
// TODO: Monolog
error_log((string) $e);
// Sentry
if ($this->sentryEnabled) {
captureException($e);
}
}
switch ($request->getAttribute('accept')) {
case 'html':
return new HtmlResponse(sprintf('<h1>%s</h1>', $message), $status);
case 'json':
default:
return new JsonResponse([
'status' => 'error',
'message' => $message,
], $status);
}
}
}
<?php declare(strict_types=1);
/**
* FrameSecurityMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware\Kernel;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class FrameSecurityMiddleware implements MiddlewareInterface
{
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
return $handler
->handle($request)
->withHeader('X-Frame-Options', 'DENY');
}
}
<?php declare(strict_types=1);
/**
* JsonPayloadMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware\Kernel;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class JsonPayloadMiddleware implements MiddlewareInterface
{
/** @var string[] */
const JSON_MIME_TYPES = ['application/json', 'text/json', 'application/x-json'];
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$contentType = $this->_normalizeContentTypeEntry($request->getHeader('Content-Type'));
if (in_array($contentType, static::JSON_MIME_TYPES, true)) {
$request = $request
->withParsedBody(json_decode($request->getBody(), true));
}
return $handler
->handle($request);
}
/**
* @param array $values
* @return mixed
*/
protected function _normalizeContentTypeEntry(array $values): string
{
$fragments = explode(';', $values[0] ?? '');
return $fragments[0];
}
}
<?php
/**
* OauthMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware\Kernel;
use Minds\Core\Session;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response;
class OauthMiddleware implements MiddlewareInterface
{
/** @var string */
protected $attributeName = '_user';
/**
* @param string $attributeName
* @return OauthMiddleware
*/
public function setAttributeName(string $attributeName): OauthMiddleware
{
$this->attributeName = $attributeName;
return $this;
}
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!$request->getAttribute($this->attributeName)) {
Session::withRouterRequest($request, new Response());
return $handler->handle(
$request
->withAttribute($this->attributeName, Session::getLoggedinUser() ?: null)
);
}
return $handler
->handle($request);
}
}
<?php declare(strict_types=1);
/**
* RegistryEntryMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware\Kernel;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Di\Ref as DiRef;
use Minds\Core\Router\RegistryEntry;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class RegistryEntryMiddleware implements MiddlewareInterface
{
/** @var string */
protected $attributeName = '_router-registry-entry';
/**
* @param string $attributeName
* @return RegistryEntryMiddleware
*/
public function setAttributeName(string $attributeName): RegistryEntryMiddleware
{
$this->attributeName = $attributeName;
return $this;
}
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
* @throws Exception
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
/** @var RegistryEntry $registryEntry */
$registryEntry = $request->getAttribute($this->attributeName);
if ($registryEntry) {
$binding = $registryEntry->getBinding();
$parameters = $registryEntry->extract($request->getUri()->getPath());
if ($binding instanceof DiRef) {
return call_user_func(
[
Di::_()->get($binding->getProvider()),
$binding->getMethod()
],
$request
->withAttribute('parameters', $parameters)
);
} elseif (is_callable($binding)) {
return call_user_func(
$binding,
$request
->withAttribute('parameters', $parameters)
);
} else {
throw new Exception("Invalid router binding");
}
}
return $handler
->handle($request);
}
}
<?php declare(strict_types=1);
/**
* RequestHandlerMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware\Kernel;
use Exception;
use Minds\Core\Router\Dispatcher;
use Minds\Core\Router\RegistryEntry;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class RequestHandlerMiddleware implements MiddlewareInterface
{
/** @var string */
protected $attributeName = '_request-handler';
/**
* @param string $attributeName
* @return RequestHandlerMiddleware
*/
public function setAttributeName(string $attributeName): RequestHandlerMiddleware
{
$this->attributeName = $attributeName;
return $this;
}
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
* @throws Exception
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$requestHandler = $request->getAttribute($this->attributeName);
if ($requestHandler) {
if ($requestHandler instanceof RegistryEntry) {
// Setup sub-router
$dispatcher = new Dispatcher();
// Pipe route-specific middleware
foreach ($requestHandler->getMiddleware() as $middleware) {
if (is_string($middleware)) {
if (!class_exists($middleware)) {
throw new Exception("{$middleware} does not exist");
}
$middlewareInstance = new $middleware;
} else {
$middlewareInstance = $middleware;
}
if (!($middlewareInstance instanceof MiddlewareInterface)) {
throw new Exception("{$middleware} is not a middleware");
}
$dispatcher->pipe($middlewareInstance);
}
// Dispatch with middleware
return $dispatcher
->pipe(
(new RegistryEntryMiddleware())
->setAttributeName('_router-registry-entry')
)
->handle(
$request
->withAttribute('_router-registry-entry', $requestHandler)
);
} elseif (is_callable($requestHandler)) {
$response = call_user_func($requestHandler, $request);
if ($response) {
return $response;
}
}
}
return $handler
->handle($request);
}
}
<?php declare(strict_types=1);
/**
* RouteResolverMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware\Kernel;
use Minds\Core\Di\Di;
use Minds\Core\Router\PrePsr7;
use Minds\Core\Router\Registry;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class RouteResolverMiddleware implements MiddlewareInterface
{
/** @var string */
protected $attributeName = '_request-handler';
/** @var Registry */
protected $registry;
/**
* RouteResolverMiddleware constructor.
* @param Registry $registry
*/
public function __construct(
$registry = null
) {
$this->registry = $registry ?: Di::_()->get('Router\Registry');
}
/**
* @param string $attributeName
* @return RouteResolverMiddleware
*/
public function setAttributeName(string $attributeName): RouteResolverMiddleware
{
$this->attributeName = $attributeName;
return $this;
}
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// Module Router
$registryEntry = $this->registry->getBestMatch(
strtolower($request->getMethod()),
$request->getUri()->getPath()
);
if ($registryEntry) {
return $handler
->handle(
$request
->withAttribute($this->attributeName, $registryEntry)
)
->withHeader('X-Route-Resolver', 'router-registry');
}
// Pre PSR-7 Fallback Handlers
$prePsr7Fallback = new PrePsr7\Fallback();
// Pre PSR-7 Controllers
if ($prePsr7Fallback->shouldRoute($request->getUri()->getPath())) {
return $prePsr7Fallback
->handle($request)
->withHeader('X-Route-Resolver', 'pre-psr7');
}
// Static HTML
if ($request->getAttribute('accept') === 'html') {
return $prePsr7Fallback
->handleStatic($request)
->withHeader('X-Route-Resolver', 'pre-psr7-static');
}
// No route handler
return $handler
->handle($request);
}
}
<?php
/**
* SessionMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware\Kernel;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Sessions\Manager;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class SessionMiddleware implements MiddlewareInterface
{
/** @var Manager */
protected $session;
/** @var string */
protected $attributeName = '_user';
/**
* SessionMiddleware constructor.
* @param Manager $session
*/
public function __construct(
$session = null
) {
$this->session = $session ?: Di::_()->get('Sessions\Manager');
}
/**
* @param string $attributeName
* @return SessionMiddleware
*/
public function setAttributeName(string $attributeName): SessionMiddleware
{
$this->attributeName = $attributeName;
return $this;
}
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!$request->getAttribute($this->attributeName)) {
$this->session
->withRouterRequest($request);
return $handler->handle(
$request
->withAttribute($this->attributeName, Session::getLoggedinUser() ?: null)
);
}
return $handler
->handle($request);
}
}
<?php
/**
* XsrfCookieMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware\Kernel;
use Minds\Core\Security\XSRF;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class XsrfCookieMiddleware implements MiddlewareInterface
{
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
XSRF::setCookie();
return $handler
->handle($request);
}
}
<?php declare(strict_types=1);
/**
* LoggedInMiddleware
* @author edgebal
*/
namespace Minds\Core\Router\Middleware;
use Minds\Core\Router\Exceptions\UnauthorizedException;
use Minds\Core\Security\XSRF;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class LoggedInMiddleware implements MiddlewareInterface
{
/** @var string */
protected $attributeName = '_user';
/** @var callable */
private $xsrfValidateRequest;
public function __construct(
$xsrfValidateRequest = null
) {
$this->xsrfValidateRequest = $xsrfValidateRequest ?: [XSRF::class, 'validateRequest'];
}
/**
* @param string $attributeName
* @return LoggedInMiddleware
*/
public function setAttributeName(string $attributeName): LoggedInMiddleware
{
$this->attributeName = $attributeName;
return $this;
}
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
* @throws UnauthorizedException
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (
!$request->getAttribute($this->attributeName) ||
!call_user_func($this->xsrfValidateRequest)
) {
throw new UnauthorizedException();
}
return $handler
->handle($request);
}
}
<?php declare(strict_types=1);
/**
* ModuleRoutes
* @author edgebal
*/
namespace Minds\Core\Router;
abstract class ModuleRoutes
{
/** @var Route */
protected $route;
/**
* ModuleRoutes constructor.
* @param Route $route
*/
public function __construct(
$route = null
) {
$this->route = $route ?: new Route();
}
/**
* Registers all module routes
*/
abstract public function register(): void;
}
<?php
/**
* Fallback
* @author edgebal
*/
namespace Minds\Core\Router\PrePsr7;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\I18n\I18n;
use Minds\Core\SEO\Defaults as SEODefaults;
use Psr\Http\Message\RequestInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Stream;
class Fallback
{
/** @var string[] */
const ALLOWED = [
'/api/v1/',
'/api/v2/',
'/emails/',
'/fs/v1',
'/oauth2/',
'/checkout',
'/deeplinks',
'/icon',
'/sitemap',
'/sitemaps',
'/thumbProxy',
'/archive',
'/wall',
'/not-supported',
'/apple-app-site-association',
];
/** @var Config */
protected $config;
/**
* Fallback constructor.
* @param Config $config
*/
public function __construct(
$config = null
) {
$this->config = $config ?: Di::_()->get('Config');
}
/**
* @param string $route
* @return bool
*/
public function shouldRoute(string $route): bool
{
$route = sprintf("/%s", ltrim($route, '/'));
$shouldFallback = false;
foreach (static::ALLOWED as $allowedRoute) {
if (stripos($route, $allowedRoute) === 0) {
$shouldFallback = true;
break;
}
}
return $shouldFallback;
}
/**
* @param RequestInterface $request
* @return Response
*/
public function handle(RequestInterface $request)
{
ob_clean();
ob_start();
(new Router())
->route($request->getUri()->getPath(), strtolower($request->getMethod()));
$response = ob_get_contents();
ob_end_clean();
$stream = fopen('php://memory', 'r+');
fwrite($stream, $response);
rewind($stream);
return new Response(new Stream($stream), http_response_code());
}
/**
* @param RequestInterface $request
* @return HtmlResponse
*/
public function handleStatic(RequestInterface $request)
{
ob_clean();
ob_start();
(new Router())
->route($request->getUri()->getPath(), strtolower($request->getMethod()));
$html = ob_get_contents();
ob_end_clean();
return new HtmlResponse($html, 200);
}
/**
* Complete routing fallback
*/
public function route()
{
(new Router())->route();
}
}
......@@ -4,7 +4,7 @@
* @author edgebal
*/
namespace Minds\Core\Router\Middleware;
namespace Minds\Core\Router\PrePsr7\Middleware;
use Exception;
use Minds\Core\EntitiesBuilder;
......
......@@ -4,7 +4,7 @@
* @author edgebal
*/
namespace Minds\Core\Router\Middleware;
namespace Minds\Core\Router\PrePsr7\Middleware;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequest;
......
<?php
namespace Minds\Core\Router\Middleware;
namespace Minds\Core\Router\PrePsr7\Middleware;
use Minds\Core\Config;
use Minds\Core\Di\Di;
......
<?php
namespace Minds\Core\Router\PrePsr7;
use Minds\Core\Di\Di;
use Minds\Core\I18n\I18n;
use Minds\Core\Router\PrePsr7\Middleware\ProMiddleware;
use Minds\Core\Router\PrePsr7\Middleware\RouterMiddleware;
use Minds\Core\Router\PrePsr7\Middleware\SEOMiddleware;
use Minds\Core\Session;
use Minds\Helpers;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequestFactory;
use Minds\Core\Security;
use Minds\Core\page;
/**
* Minds Core Router.
*/
class Router
{
// these are core pages, other pages are registered by plugins
public static $routes = [
'/archive/thumbnail' => 'Minds\\Controllers\\fs\\v1\\thumbnail',
'/api/v1/archive/thumbnails' => 'Minds\\Controllers\\api\\v1\\media\\thumbnails',
'/oauth2/token' => 'Minds\\Controllers\\oauth2\\token',
'/oauth2/implicit' => 'Minds\\Controllers\\oauth2\\implicit',
'/icon' => 'Minds\\Controllers\\icon',
'//icon' => 'Minds\\Controllers\\icon',
'/api' => 'Minds\\Controllers\\api\\api',
'/fs' => 'Minds\\Controllers\\fs\\fs',
'/thumbProxy' => 'Minds\\Controllers\\thumbProxy',
'/wall' => 'Minds\\Controllers\\Legacy\\wall',
'/not-supported' => "Minds\Controllers\\notSupported",
// "/app" => "minds\\pages\\app",
'/emails/unsubscribe' => 'Minds\\Controllers\\emails\\unsubscribe',
'/sitemap' => 'Minds\\Controllers\\sitemap',
'/apple-app-site-association' => '\\Minds\\Controllers\\deeplinks',
'/sitemaps' => '\\Minds\\Controllers\\sitemaps',
'/checkout' => '\\Minds\\Controllers\\checkout',
];
/**
* Route the pages
* (fallback to elgg page handler if we fail).
*
* @param string $uri
* @param string $method
*
* @return null|mixed
*/
public function route($uri = null, $method = null)
{
if ((!$uri) && (isset($_SERVER['REDIRECT_ORIG_URI']))) {
$uri = strtok($_SERVER['REDIRECT_ORIG_URI'], '?');
}
if (!$uri) {
$uri = strtok($_SERVER['REQUEST_URI'], '?');
}
$this->detectContentType();
header('X-Frame-Options: DENY');
$route = rtrim($uri, '/');
$segments = explode('/', $route);
$method = $method ? $method : strtolower($_SERVER['REQUEST_METHOD']);
if ($method == 'post') {
$this->postDataFix();
}
$request = ServerRequestFactory::fromGlobals();
$response = new JsonResponse([]);
/** @var RouterMiddleware[] $prePsr7Middleware */
$prePsr7Middleware = [
new ProMiddleware(),
new SEOMiddleware(),
];
foreach ($prePsr7Middleware as $middleware) {
$result = $middleware->onRequest($request, $response);
if ($result === false) {
return null;
}
}
if ($request->getMethod() === 'OPTIONS') {
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,X-No-Cache');
return null;
}
// Sessions
// TODO: Support middleware
$session = Di::_()->get('Sessions\Manager');
$session->withRouterRequest($request);
// OAuth Middleware
// TODO: allow interface to bypass
// TODO: Support middleware
if (!Session::isLoggedIn()) { // Middleware will resolve this
Session::withRouterRequest($request, $response);
}
// XSRF Cookie - may be able to remove now with OAuth flow
Security\XSRF::setCookie();
if (Session::isLoggedin()) {
Helpers\Analytics::increment('active');
}
if (isset($_GET['__e_ct_guid']) && is_numeric($_GET['__e_ct_guid'])) {
Helpers\Analytics::increment('active', $_GET['__e_ct_guid']);
Helpers\Campaigns\EmailRewards::reward($_GET['campaign'], $_GET['__e_ct_guid']);
}
Di::_()->get('Email\RouterHooks')
->withRouterRequest($request);
Di::_()->get('Referrals\Cookie')
->withRouterRequest($request)
->create();
$loop = count($segments);
while ($loop >= 0) {
$offset = $loop - 1;
if ($loop < count($segments)) {
$slug_length = strlen($segments[$offset + 1].'/');
$route_length = strlen($route);
$route = substr($route, 0, $route_length - $slug_length);
}
if (isset(self::$routes[$route])) {
$handler = new self::$routes[$route]();
$pages = array_splice($segments, $loop) ?: [];
if (method_exists($handler, $method)) {
// Set the request
if (method_exists($handler, 'setRequest')) {
$handler->setRequest($request);
}
// Set the response
if (method_exists($handler, 'setResponse')) {
$handler->setResponse($response);
}
return $handler->$method($pages);
} else {
return null;
}
}
--$loop;
}
if (!$this->legacyRoute($uri)) {
(new I18n())->serveIndex();
}
return null;
}
/**
* Legacy Router fallback.
*
* @param string $uri
*
* @return bool
*/
public function legacyRoute($uri)
{
$path = explode('/', substr($uri, 1));
$handler = array_shift($path);
$page = implode('/', $path);
new page(false); //just to load init etc
return false;
}
/**
* Detects request content type and apply the corresponding polyfills.
*/
public function detectContentType()
{
if (isset($_SERVER['CONTENT_TYPE']) && $_SERVER['CONTENT_TYPE'] == 'application/json') {
//\elgg_set_viewtype('json');
if (strtolower($_SERVER['REQUEST_METHOD']) == 'post') {
$this->postDataFix();
}
}
}
/**
* Populates $_POST and $_REQUEST with request's JSON payload.
*/
public function postDataFix()
{
$postdata = file_get_contents('php://input');
$request = json_decode($postdata, true);
if ($request) {
foreach ($request as $k => $v) {
$_POST[$k] = $v;
$_REQUEST[$k] = $v;
}
}
}
/**
* Return vars for request
* @return array
*/
public static function getPutVars()
{
$postdata = file_get_contents('php://input');
$request = json_decode($postdata, true);
return $request;
}
/**
* Register routes.
*
* @param array $routes - an array of routes to handlers
*
* @return array - the array of all your routes
*/
public static function registerRoutes($routes = [])
{
return self::$routes = array_merge(self::$routes, $routes);
}
}
<?php declare(strict_types=1);
/**
* Registry
* @author edgebal
*/
namespace Minds\Core\Router;
class Registry
{
/** @var Registry */
protected static $instance;
/** @var array */
protected $registry = [];
/**
* @return Registry
*/
public static function _(): Registry
{
if (!static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
/**
* @param string $method
* @param string $route
* @param mixed $binding
* @param string[] $middleware
* @return Registry
*/
public function register(string $method, string $route, $binding, array $middleware): Registry
{
$method = strtolower($method);
if (!isset($this->registry[$method])) {
$this->registry[$method] = [];
}
$registryEntry = new RegistryEntry();
$registryEntry
->setRoute($route)
->setBinding($binding)
->setMiddleware($middleware);
$this->registry[$method][] = $registryEntry;
return $this;
}
/**
* @param string $method
* @param string $route
* @return RegistryEntry|null
*/
public function getBestMatch(string $method, string $route):? RegistryEntry
{
if (!isset($this->registry[$method]) || !$this->registry[$method]) {
return null;
}
$route = trim($route, '/');
/** @var RegistryEntry[] $sortedRegistryEntries */
$sortedRegistryEntries = $this->registry[$method];
usort($sortedRegistryEntries, [$this, '_registryEntrySort']);
foreach ($sortedRegistryEntries as $registryEntry) {
if ($registryEntry->matches($route)) {
return $registryEntry;
}
}
return null;
}
/**
* @param RegistryEntry $a
* @param RegistryEntry $b
* @return int
*/
protected function _registryEntrySort(RegistryEntry $a, RegistryEntry $b): int
{
if ($a->getDepth() !== $b->getDepth()) {
return $b->getDepth() - $a->getDepth();
}
return $b->getSpecificity() - $a->getSpecificity();
}
}
<?php declare(strict_types=1);
/**
* RegistryEntry
* @author edgebal
*/
namespace Minds\Core\Router;
use Exception;
use Minds\Traits\MagicAttributes;
/**
* Class RegistryEntry
* @package Minds\Core\Router
* @method string getRoute()
* @method mixed getBinding()
* @method RegistryEntry setBinding(mixed $binding)
* @method string[] getMiddleware[]
* @method RegistryEntry setMiddleware(string[] $middleware)
*/
class RegistryEntry
{
use MagicAttributes;
/** @var string */
protected $route;
/** @var mixed */
protected $binding;
/** @var string[] */
protected $middleware;
/**
* @param string $route
* @return RegistryEntry
*/
public function setRoute(string $route): RegistryEntry
{
$this->route = trim($route, '/');
return $this;
}
/**
* @return string
*/
public function getWildcardRoute(): string
{
return preg_replace('#/:[^/]+#', '/*', $this->route);
}
/**
* @return int
*/
public function getDepth(): int
{
if (!$this->route) {
return -1;
}
return substr_count($this->route, '/');
}
/**
* @return int
*/
public function getSpecificity(): int
{
if (!$this->route) {
return 1;
}
$fragments = explode('/', $this->getWildcardRoute());
$count = count($fragments);
$specificity = 0;
for ($i = 0; $i < $count; $i++) {
if ($fragments[$i] !== '*') {
$specificity += 2 ** ($count - 1 - $i);
}
}
return $specificity;
}
/**
* @param string $route
* @return bool
*/
public function matches(string $route): bool
{
$route = trim($route, '/');
$pattern = sprintf("#^%s$#i", strtr(preg_quote($this->getWildcardRoute(), '#'), ['\*' => '[^/]+']));
return (bool) preg_match($pattern, $route);
}
/**
* @param string $route
* @return array
*/
public function extract(string $route): array
{
$route = trim($route, '/');
$pattern = sprintf(
'#^%s$#i',
preg_replace_callback('#/\\\:([^/]+)#', [$this, '_regexNamedCapture'], preg_quote($this->route, '#'))
);
$matches = [];
preg_match($pattern, $route, $matches);
$parameters = [];
foreach ($matches as $key => $value) {
if (is_numeric($key)) {
continue;
}
$parameters[$key] = $value;
}
return $parameters;
}
/**
* @param array $matches
* @return string
* @throws Exception
*/
protected function _regexNamedCapture(array $matches): string
{
$name = $matches[1] ?? '_';
if (is_numeric($name) || !ctype_alnum($name)) {
throw new Exception('Invalid route parameter name');
}
return sprintf('/(?<%s>[^/]+)', $name);
}
}
<?php declare(strict_types=1);
/**
* Route
* @author edgebal
*/
namespace Minds\Core\Router;
use Exception;
use Minds\Traits\MagicAttributes;
/**
* Class Route
* @package Minds\Core\Router
* @method string getPrefix()
* @method Route setPrefix(string $prefix)
* @method string[] getMiddleware()
* @method Route setMiddleware(string[] $prefix)
*/
class Route
{
use MagicAttributes;
/** @var string */
protected $prefix = '/';
/** @var string[] */
protected $middleware = [];
/** @var Registry */
protected $registry;
/** @var string[] */
const ALLOWED_METHODS = ['get','post','put','delete'];
/**
* Route constructor.
* @param Registry|null $registry
*/
public function __construct(
Registry $registry = null
) {
$this->registry = $registry ?: Registry::_();
}
/**
* @param string $prefix
* @return Route
*/
public function withPrefix(string $prefix): Route
{
$instance = clone($this);
$instance->setPrefix(sprintf("/%s/%s", trim($instance->getPrefix(), '/'), trim($prefix, '/')));
return $instance;
}
/**
* @param string[] $middleware
* @return Route
*/
public function withMiddleware(array $middleware): Route
{
$instance = clone($this);
$instance->setMiddleware(array_merge($instance->getMiddleware(), $middleware));
return $instance;
}
/**
* @param callable $fn
* @return Route
*/
public function do(callable $fn): Route
{
call_user_func($fn, $this);
return $this;
}
/**
* @param string[] $methods
* @param string $route
* @param $binding
* @return bool
* @throws Exception
*/
public function register(array $methods, string $route, $binding): bool
{
if (array_diff($methods, static::ALLOWED_METHODS)) {
throw new Exception('Invalid method');
}
$route = sprintf("/%s/%s", trim($this->getPrefix(), '/'), trim($route, '/'));
$route = trim($route, '/');
foreach ($methods as $method) {
$this->registry->register($method, $route, $binding, $this->middleware);
}
return true;
}
/**
* @param string $route
* @param $binding
* @return bool
* @throws Exception
*/
public function all(string $route, $binding): bool
{
return $this->register(static::ALLOWED_METHODS, $route, $binding);
}
/**
* @param string $route
* @param $binding
* @return bool
* @throws Exception
*/
public function get(string $route, $binding): bool
{
return $this->register(['get'], $route, $binding);
}
/**
* @param string $route
* @param $binding
* @return bool
* @throws Exception
*/
public function post(string $route, $binding): bool
{
return $this->register(['post'], $route, $binding);
}
/**
* @param string $route
* @param $binding
* @return bool
* @throws Exception
*/
public function put(string $route, $binding): bool
{
return $this->register(['put'], $route, $binding);
}
/**
* @param string $route
* @param $binding
* @return bool
* @throws Exception
*/
public function delete(string $route, $binding): bool
{
return $this->register(['delete'], $route, $binding);
}
}
......@@ -12,8 +12,12 @@ class RouterProvider extends Provider
{
public function register()
{
$this->di->bind('Router\Manager', function ($di) {
return new Manager();
}, [ 'useFactory' => true ]);
$this->di->bind('Router', function ($di) {
return new Dispatcher();
}, ['useFactory' => true]);
$this->di->bind('Router\Registry', function ($di) {
return Registry::_();
}, ['useFactory' => true]);
}
}
......@@ -259,6 +259,14 @@ class Defaults
];
});
// Do not index email confirmation and redirect OG to /
Manager::add(Core\Email\Confirmation\Url::EMAIL_CONFIRMATION_PATH, function ($slugs = []) {
return [
'og:url' => $this->config->site_url,
'robots' => 'noindex'
];
});
Manager::add('/wallet/tokens/transactions', function ($slugs = []) {
$meta = [
'title' => 'Transactions Ledger',
......
......@@ -283,5 +283,18 @@ class ProhibitedDomains
'localmodels.online',
'kaikki-mallit.com',
'hotswishes.com',
'/diigo.com',
'.storeboard.com',
'.folkd.com',
'/ttlink.com',
'/mix.com',
'/list.ly',
'.adpost.com',
'.23hq.com',
'/lineupnow.com',
'/amusecandy.com',
'/360mate.com',
'rebrand.ly',
'fiverr.com', // temporarily until better defence is built
];
}
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.