addValidations('email', [ 'required' => __('Please enter your email address', 'mailpoet'), 'validEmail' => __('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((string)$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((string)$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' => __('All Lists', 'mailpoet'), 'value' => '', ]; $subscribersWithoutSegment = self::filter('withoutSegments') ->whereNull('deleted_at') ->count(); $subscribersWithoutSegmentLabel = sprintf( // translators: %s is the number of subscribers without a segment. __('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); [$subscriber] = $service->subscribe($subscriberData, $segmentIds); return $subscriber; } }