| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708 |
- <?php
- namespace MailPoet\Models;
- if (!defined('ABSPATH')) exit;
- use MailPoet\DI\ContainerWrapper;
- use MailPoet\Settings\SettingsController;
- use MailPoet\Util\Helpers;
- use MailPoet\Util\Security;
- use MailPoet\WP\Functions as WPFunctions;
- /**
- * @property int $id
- * @property string $email
- * @property string $firstName
- * @property string $lastName
- * @property string $status
- * @property string|null $subscribedIp
- * @property string|null $confirmedIp
- * @property string|null $confirmedAt
- * @property string|null $lastSubscribedAt
- * @property string|null $deletedAt
- * @property string|null $source
- * @property string|null $linkToken
- * @property int $countConfirmations
- * @property int $wpUserId
- * @property array $segments
- * @property array $subscriptions
- * @property string|null $unconfirmedData
- * @property int $isWoocommerceUser
- */
- class Subscriber extends Model {
- public static $_table = MP_SUBSCRIBERS_TABLE; // phpcs:ignore PSR2.Classes.PropertyDeclaration
- const STATUS_SUBSCRIBED = 'subscribed';
- const STATUS_UNSUBSCRIBED = 'unsubscribed';
- const STATUS_UNCONFIRMED = 'unconfirmed';
- const STATUS_BOUNCED = 'bounced';
- const STATUS_INACTIVE = 'inactive';
- const OBSOLETE_LINK_TOKEN_LENGTH = 6;
- const LINK_TOKEN_LENGTH = 32;
- /** @var string|bool */
- public $token;
- public function __construct() {
- parent::__construct();
- $this->addValidations('email', [
- 'required' => WPFunctions::get()->__('Please enter your email address', 'mailpoet'),
- 'validEmail' => WPFunctions::get()->__('Your email address is invalid!', 'mailpoet'),
- ]);
- }
- public static function findOne($id = false) {
- if (is_int($id) || (string)(int)$id === $id) {
- return parent::findOne($id);
- } else if (strlen(trim($id)) > 0) {
- return parent::where('email', $id)->findOne();
- }
- return false;
- }
- public function segments() {
- return $this->has_many_through(
- __NAMESPACE__ . '\Segment',
- __NAMESPACE__ . '\SubscriberSegment',
- 'subscriber_id',
- 'segment_id'
- )
- ->where(MP_SUBSCRIBER_SEGMENT_TABLE . '.status', self::STATUS_SUBSCRIBED);
- }
- public function save() {
- // convert email to lowercase format
- $this->email = strtolower($this->email);
- return parent::save();
- }
- public function delete() {
- // WP Users cannot be deleted
- if (!$this->isWPUser() && !$this->isWooCommerceUser()) {
- // delete all relations to segments
- SubscriberSegment::deleteSubscriptions($this);
- // delete all relations to custom fields
- SubscriberCustomField::deleteSubscriberRelations($this);
- return parent::delete();
- }
- return null;
- }
- public function trash() {
- // WP Users cannot be trashed
- if ($this->isWPUser() || $this->isWooCommerceUser()) {
- return false;
- } else {
- return parent::trash();
- }
- }
- public function isWPUser() {
- return (bool)$this->wpUserId;
- }
- public function isWooCommerceUser() {
- return (bool)$this->isWoocommerceUser;
- }
- /**
- * @deprecated Use the version in \MailPoet\Subscribers\SubscribersRepository::getCurrentWPUser
- */
- public static function getCurrentWPUser() {
- trigger_error('Calling Subscriber::getCurrentWPUser() is deprecated and will be removed. Use MailPoet\Subscribers\SubscribersRepository::getCurrentWPUser(). ', E_USER_DEPRECATED);
- $wpUser = WPFunctions::get()->wpGetCurrentUser();
- if (empty($wpUser->ID)) {
- return false; // Don't look up a subscriber for guests
- }
- return self::where('wp_user_id', $wpUser->ID)->findOne();
- }
- public static function filterOutReservedColumns(array $subscriberData) {
- $reservedColumns = [
- 'id',
- 'wp_user_id',
- 'is_woocommerce_user',
- 'status',
- 'subscribed_ip',
- 'confirmed_ip',
- 'confirmed_at',
- 'created_at',
- 'updated_at',
- 'deleted_at',
- 'unconfirmed_data',
- ];
- $subscriberData = array_diff_key(
- $subscriberData,
- array_flip($reservedColumns)
- );
- return $subscriberData;
- }
- public static function search($orm, $search = '') {
- if (strlen(trim($search)) === 0) {
- return $orm;
- }
- return $orm->where_raw(
- '(`email` LIKE ? OR `first_name` LIKE ? OR `last_name` LIKE ?)',
- ['%' . $search . '%', '%' . $search . '%', '%' . $search . '%']
- );
- }
- public static function filters($data = []) {
- $group = (!empty($data['group'])) ? $data['group'] : 'all';
- $segments = Segment::orderByAsc('name')
- ->whereNull('deleted_at')
- ->whereIn('type', Segment::getSegmentTypes())
- ->findMany();
- $segmentList = [];
- $segmentList[] = [
- 'label' => WPFunctions::get()->__('All Lists', 'mailpoet'),
- 'value' => '',
- ];
- $subscribersWithoutSegment = self::filter('withoutSegments')
- ->whereNull('deleted_at')
- ->count();
- $subscribersWithoutSegmentLabel = sprintf(
- __('Subscribers without a list (%s)', 'mailpoet'),
- number_format($subscribersWithoutSegment)
- );
- $segmentList[] = [
- 'label' => $subscribersWithoutSegmentLabel,
- 'value' => 'none',
- ];
- foreach ($segments as $segment) {
- $subscribersCount = 0;
- $subscribers = $segment->subscribers()
- ->filter('groupBy', $group);
- if ($subscribers) {
- $subscribersCount = $subscribers->count();
- }
- $label = sprintf(
- '%s (%s)',
- $segment->name,
- number_format($subscribersCount)
- );
- $segmentList[] = [
- 'label' => $label,
- 'value' => $segment->id(),
- ];
- }
- $filters = [
- 'segment' => $segmentList,
- ];
- return $filters;
- }
- public static function filterBy($orm, $filters = null) {
- if (empty($filters)) {
- return $orm;
- }
- foreach ($filters as $key => $value) {
- if ($key === 'segment') {
- if ($value === 'none') {
- return self::filter('withoutSegments');
- } else {
- $segment = Segment::findOne($value);
- if ($segment instanceof Segment) {
- return $segment->subscribers();
- }
- }
- }
- }
- return $orm;
- }
- public static function groupBy($orm, $group = null) {
- if ($group === 'trash') {
- return $orm->whereNotNull('deleted_at');
- } else if ($group === 'all') {
- return $orm->whereNull('deleted_at');
- } else {
- return $orm->filter($group);
- }
- }
- public static function filterWithCustomFields($orm) {
- $orm = $orm->select(MP_SUBSCRIBERS_TABLE . '.*');
- $customFields = CustomField::findArray();
- foreach ($customFields as $customField) {
- $orm = $orm->select_expr(
- 'IFNULL(GROUP_CONCAT(CASE WHEN ' .
- MP_CUSTOM_FIELDS_TABLE . '.id=' . $customField['id'] . ' THEN ' .
- MP_SUBSCRIBER_CUSTOM_FIELD_TABLE . '.value END), NULL) as "' . $customField['name'] . '"');
- }
- $orm = $orm
- ->leftOuterJoin(
- MP_SUBSCRIBER_CUSTOM_FIELD_TABLE,
- [
- MP_SUBSCRIBERS_TABLE . '.id',
- '=',
- MP_SUBSCRIBER_CUSTOM_FIELD_TABLE . '.subscriber_id',
- ]
- )
- ->leftOuterJoin(
- MP_CUSTOM_FIELDS_TABLE,
- [
- MP_CUSTOM_FIELDS_TABLE . '.id',
- '=',
- MP_SUBSCRIBER_CUSTOM_FIELD_TABLE . '.custom_field_id',
- ]
- )
- ->groupBy(MP_SUBSCRIBERS_TABLE . '.id');
- return $orm;
- }
- /**
- * @deprecated
- */
- public static function filterWithCustomFieldsForExport($orm) {
- $orm = $orm->select(MP_SUBSCRIBERS_TABLE . '.*');
- $customFields = CustomField::findArray();
- foreach ($customFields as $customField) {
- $orm = $orm->selectExpr(
- 'MAX(CASE WHEN ' .
- MP_CUSTOM_FIELDS_TABLE . '.id=' . $customField['id'] . ' THEN ' .
- MP_SUBSCRIBER_CUSTOM_FIELD_TABLE . '.value END) as "' . $customField['id'] . '"'
- );
- }
- $orm = $orm
- ->leftOuterJoin(
- MP_SUBSCRIBER_CUSTOM_FIELD_TABLE,
- [
- MP_SUBSCRIBERS_TABLE . '.id', '=',
- MP_SUBSCRIBER_CUSTOM_FIELD_TABLE . '.subscriber_id',
- ]
- )
- ->leftOuterJoin(
- MP_CUSTOM_FIELDS_TABLE,
- [
- MP_CUSTOM_FIELDS_TABLE . '.id','=',
- MP_SUBSCRIBER_CUSTOM_FIELD_TABLE . '.custom_field_id',
- ]
- );
- return $orm;
- }
- public static function getSubscribedInSegments($segmentIds) {
- $subscribers = SubscriberSegment::tableAlias('relation')
- ->whereIn('relation.segment_id', $segmentIds)
- ->where('relation.status', 'subscribed')
- ->join(
- MP_SUBSCRIBERS_TABLE,
- 'subscribers.id = relation.subscriber_id',
- 'subscribers'
- )
- ->select('subscribers.id')
- ->whereNull('subscribers.deleted_at')
- ->where('subscribers.status', 'subscribed')
- ->distinct();
- return $subscribers;
- }
- /**
- * @param string $customerEmail
- * @return bool|Subscriber
- */
- public static function getWooCommerceSegmentSubscriber($customerEmail) {
- $wcSegment = Segment::getWooCommerceSegment();
- return Subscriber::tableAlias('subscribers')
- ->select('subscribers.*')
- ->where('subscribers.email', $customerEmail)
- ->join(
- MP_SUBSCRIBER_SEGMENT_TABLE,
- 'relation.subscriber_id = subscribers.id',
- 'relation'
- )
- ->where('relation.segment_id', $wcSegment->id)
- ->where('relation.status', Subscriber::STATUS_SUBSCRIBED)
- ->whereIn('subscribers.status', [Subscriber::STATUS_SUBSCRIBED, Subscriber::STATUS_UNCONFIRMED])
- ->where('subscribers.is_woocommerce_user', 1)
- ->findOne();
- }
- public function customFields() {
- return $this->hasManyThrough(
- __NAMESPACE__ . '\CustomField',
- __NAMESPACE__ . '\SubscriberCustomField',
- 'subscriber_id',
- 'custom_field_id'
- )->select_expr(MP_SUBSCRIBER_CUSTOM_FIELD_TABLE . '.value');
- }
- public static function createOrUpdate($data = []) {
- $subscriber = false;
- if (is_array($data) && !empty($data)) {
- $data = WPFunctions::get()->stripslashesDeep($data);
- }
- if (isset($data['id']) && (int)$data['id'] > 0) {
- $subscriber = self::findOne((int)$data['id']);
- unset($data['id']);
- }
- if ($subscriber === false && !empty($data['email'])) {
- $subscriber = self::where('email', $data['email'])->findOne();
- if ($subscriber !== false) {
- unset($data['email']);
- }
- }
- // segments
- $segmentIds = false;
- if (array_key_exists('segments', $data)) {
- $segmentIds = (array)$data['segments'];
- unset($data['segments']);
- }
- // if new subscriber, make sure that required fields are set
- if (!$subscriber) {
- $data = self::setRequiredFieldsDefaultValues($data);
- }
- // get custom fields
- list($data, $customFields) = self::extractCustomFieldsFromFromObject($data);
- // wipe any unconfirmed data at this point
- $data['unconfirmed_data'] = null;
- $oldStatus = false;
- $newStatus = false;
- if ($subscriber === false) {
- $subscriber = self::create();
- $subscriber->hydrate($data);
- } else {
- $oldStatus = $subscriber->status;
- $subscriber->set($data);
- $newStatus = $subscriber->status;
- }
- // Update last_subscribed_at when status changes to subscribed
- if ($oldStatus !== self::STATUS_SUBSCRIBED && $subscriber->status === self::STATUS_SUBSCRIBED) {
- $subscriber->set('last_subscribed_at', WPFunctions::get()->currentTime('mysql'));
- }
- if ($subscriber->save()) {
- if (!empty($customFields)) {
- $subscriber->saveCustomFields($customFields);
- }
- // check for status change
- if (
- ($oldStatus === self::STATUS_SUBSCRIBED)
- &&
- ($newStatus === self::STATUS_UNSUBSCRIBED)
- ) {
- // make sure we unsubscribe the user from all segments
- SubscriberSegment::unsubscribeFromSegments($subscriber);
- } else {
- if ($segmentIds !== false) {
- SubscriberSegment::resetSubscriptions($subscriber, $segmentIds);
- }
- }
- }
- return $subscriber;
- }
- public function withCustomFields() {
- $customFields = CustomField::select('id')->findArray();
- if (empty($customFields)) return $this;
- $customFieldIds = array_column($customFields, 'id');
- $relations = SubscriberCustomField::select('custom_field_id')
- ->select('value')
- ->whereIn('custom_field_id', $customFieldIds)
- ->where('subscriber_id', $this->id())
- ->findMany();
- foreach ($relations as $relation) {
- $this->{'cf_' . $relation->customFieldId} = $relation->value;
- }
- return $this;
- }
- public function withSegments() {
- $this->segments = $this->segments()->findArray();
- return $this;
- }
- public function withSubscriptions() {
- $this->subscriptions = SubscriberSegment::where('subscriber_id', $this->id())
- ->findArray();
- return $this;
- }
- public function getCustomField($customFieldId, $default = null) {
- $customField = SubscriberCustomField::select('value')
- ->where('custom_field_id', $customFieldId)
- ->where('subscriber_id', $this->id())
- ->findOne();
- if ($customField instanceof SubscriberCustomField) {
- return $customField->value;
- } else {
- return $default;
- }
- }
- public function saveCustomFields($customFieldsData = []) {
- // get custom field ids
- $customFieldIds = array_keys($customFieldsData);
- // get custom fields
- $customFields = CustomField::whereIdIn($customFieldIds)->findMany();
- foreach ($customFields as $customField) {
- $value = (isset($customFieldsData[$customField->id])
- ? $customFieldsData[$customField->id]
- : null
- );
- // format value
- $value = $customField->formatValue($value);
- $this->setCustomField($customField->id, $value);
- }
- }
- public function setCustomField($customFieldId, $value) {
- return SubscriberCustomField::createOrUpdate([
- 'subscriber_id' => $this->id(),
- 'custom_field_id' => $customFieldId,
- 'value' => $value,
- ]);
- }
- public function setUnconfirmedData(array $subscriberData) {
- $subscriberData = self::filterOutReservedColumns($subscriberData);
- $encoded = json_encode($subscriberData);
- if (is_string($encoded)) {
- $this->unconfirmedData = $encoded;
- }
- }
- public function getUnconfirmedData() {
- if (!empty($this->unconfirmedData)) {
- $subscriberData = json_decode($this->unconfirmedData, true);
- $subscriberData = self::filterOutReservedColumns((array)$subscriberData);
- return $subscriberData;
- }
- return null;
- }
- /**
- * @deprecated Use MailPoet\Util\License\Features\Subscribers::getSubscribersCount or \MailPoet\Subscribers\SubscribersRepository::getTotalSubscribers
- */
- public static function getTotalSubscribers() {
- return self::whereIn('status', [
- self::STATUS_SUBSCRIBED,
- self::STATUS_UNCONFIRMED,
- self::STATUS_INACTIVE,
- ])
- ->whereNull('deleted_at')
- ->count();
- }
- public static function getInactiveSubscribersCount() {
- return self::where('status', self::STATUS_INACTIVE)
- ->whereNull('deleted_at')
- ->count();
- }
- public static function subscribed($orm) {
- return $orm
- ->whereNull('deleted_at')
- ->where('status', self::STATUS_SUBSCRIBED);
- }
- public static function unsubscribed($orm) {
- return $orm
- ->whereNull('deleted_at')
- ->where('status', self::STATUS_UNSUBSCRIBED);
- }
- public static function unconfirmed($orm) {
- return $orm
- ->whereNull('deleted_at')
- ->where('status', self::STATUS_UNCONFIRMED);
- }
- public static function bounced($orm) {
- return $orm
- ->whereNull('deleted_at')
- ->where('status', self::STATUS_BOUNCED);
- }
- public static function inactive($orm) {
- return $orm
- ->whereNull('deleted_at')
- ->where('status', self::STATUS_INACTIVE);
- }
- public static function withoutSegments($orm) {
- return $orm->select(MP_SUBSCRIBERS_TABLE . '.*')
- ->whereRaw(
- MP_SUBSCRIBERS_TABLE . '.id NOT IN (
- SELECT `subscriber_id`
- FROM ' . MP_SUBSCRIBER_SEGMENT_TABLE . '
- WHERE `status` = "' . self::STATUS_SUBSCRIBED . '" AND `segment_id` IN (
- SELECT `id` FROM ' . MP_SEGMENTS_TABLE . ' WHERE `deleted_at` IS NULL
- )
- )'
- );
- }
- public static function createMultiple($columns, $values) {
- return self::rawExecute(
- 'INSERT INTO `' . self::$_table . '` ' .
- '(' . implode(', ', $columns) . ') ' .
- 'VALUES ' . rtrim(
- str_repeat(
- '(' . rtrim(str_repeat('?,', count($columns)), ',') . ')' . ', ',
- count($values)
- ),
- ', '
- ),
- Helpers::flattenArray($values)
- );
- }
- public static function updateMultiple($columns, $subscribers, $updatedAt = false) {
- $ignoreColumnsOnUpdate = [
- 'wp_user_id',
- 'is_woocommerce_user',
- 'email',
- 'created_at',
- 'last_subscribed_at',
- ];
- // check if there is anything to update after excluding ignored columns
- if (!array_diff($columns, $ignoreColumnsOnUpdate)) return;
- $subscribers = array_map('array_values', $subscribers);
- $emailPosition = array_search('email', $columns);
- $sql =
- function($type) use (
- $columns,
- $subscribers,
- $emailPosition,
- $ignoreColumnsOnUpdate
- ) {
- return array_filter(
- array_map(function($columnPosition, $columnName) use (
- $type,
- $subscribers,
- $emailPosition,
- $ignoreColumnsOnUpdate
- ) {
- if (in_array($columnName, $ignoreColumnsOnUpdate)) return;
- $query = array_map(
- function($subscriber) use ($type, $columnPosition, $emailPosition) {
- return ($type === 'values') ?
- [
- $subscriber[$emailPosition],
- $subscriber[$columnPosition],
- ] :
- 'WHEN email = ? THEN ?';
- }, $subscribers);
- return ($type === 'values') ?
- Helpers::flattenArray($query) :
- $columnName . '= (CASE ' . implode(' ', $query) . ' END)';
- }, array_keys($columns), $columns)
- );
- };
- return self::rawExecute(
- 'UPDATE `' . self::$_table . '` ' .
- 'SET ' . implode(', ', $sql('statement')) . ' ' .
- (($updatedAt) ? ', updated_at = "' . $updatedAt . '" ' : '') .
- ', unconfirmed_data = NULL ' .
- 'WHERE email IN ' .
- '(' . rtrim(str_repeat('?,', count($subscribers)), ',') . ')',
- array_merge(
- Helpers::flattenArray($sql('values')),
- array_column($subscribers, $emailPosition)
- )
- );
- }
- public static function findSubscribersInSegments(array $subscribersIds, array $segmentsIds) {
- return self::getSubscribedInSegments($segmentsIds)
- ->whereIn('subscribers.id', $subscribersIds)
- ->select('subscribers.*');
- }
- public static function extractSubscribersIds(array $subscribers) {
- return array_filter(
- array_map(function($subscriber) {
- return (!empty($subscriber->id)) ? $subscriber->id : false;
- }, $subscribers)
- );
- }
- public static function setRequiredFieldsDefaultValues($data) {
- $settings = SettingsController::getInstance();
- $requiredFieldDefaultValues = [
- 'first_name' => '',
- 'last_name' => '',
- 'unsubscribe_token' => Security::generateUnsubscribeToken(self::class),
- 'link_token' => Security::generateRandomString(self::LINK_TOKEN_LENGTH),
- 'status' => (!$settings->get('signup_confirmation.enabled')) ? self::STATUS_SUBSCRIBED : self::STATUS_UNCONFIRMED,
- ];
- foreach ($requiredFieldDefaultValues as $field => $value) {
- if (!isset($data[$field])) {
- $data[$field] = $value;
- }
- }
- return $data;
- }
- public static function extractCustomFieldsFromFromObject($data) {
- $customFields = [];
- foreach ($data as $key => $value) {
- if (strpos($key, 'cf_') === 0) {
- $customFields[(int)substr($key, 3)] = $value;
- unset($data[$key]);
- }
- }
- return [$data, $customFields];
- }
- public function getAllSegmentNamesWithStatus() {
- return Segment::tableAlias('segment')
- ->select('name')
- ->select('subscriber_segment.segment_id', 'segment_id')
- ->select('subscriber_segment.status', 'status')
- ->select('subscriber_segment.updated_at', 'updated_at')
- ->join(
- SubscriberSegment::$_table,
- ['subscriber_segment.segment_id', '=', 'segment.id'],
- 'subscriber_segment'
- )
- ->where('subscriber_segment.subscriber_id', $this->id)
- ->orderByAsc('name')
- ->findArray();
- }
- /**
- * This method is here only for BC fix of 3rd party plugin integration.
- * @see https://kb.mailpoet.com/article/195-add-subscribers-through-your-own-form-or-plugin
- * @deprecated
- */
- public static function subscribe($subscriberData = [], $segmentIds = []) {
- trigger_error('Calling Subscriber::subscribe() is deprecated and will be removed. Use MailPoet\API\MP\v1\API instead. ', E_USER_DEPRECATED);
- $service = ContainerWrapper::getInstance()->get(\MailPoet\Subscribers\SubscriberActions::class);
- return $service->subscribe($subscriberData, $segmentIds);
- }
- }
|