- Removes requirement of passing newsletter id when tracking clicks
- Extracts common tracking data processing/validation code into the Track class - Refactors Clicks, Opens and View in Browser classes to enforce subscriber id and token check - Allows admin users to preview newsletters without tracking statistics
This commit is contained in:
@ -5,6 +5,7 @@ use MailPoet\API\Error as APIError;
|
||||
|
||||
use MailPoet\Listing;
|
||||
use MailPoet\Models\Newsletter;
|
||||
use MailPoet\Models\SendingQueue;
|
||||
use MailPoet\Models\Setting;
|
||||
use MailPoet\Models\NewsletterTemplate;
|
||||
use MailPoet\Models\NewsletterSegment;
|
||||
@ -252,8 +253,10 @@ class Newsletters extends APIEndpoint {
|
||||
$data
|
||||
);
|
||||
$listing_data = $listing->get();
|
||||
$subscriber = Subscriber::getCurrentWPUser();
|
||||
|
||||
foreach($listing_data['items'] as $key => $newsletter) {
|
||||
$queue = false;
|
||||
|
||||
if($newsletter->type === Newsletter::TYPE_STANDARD) {
|
||||
$newsletter
|
||||
@ -277,8 +280,15 @@ class Newsletters extends APIEndpoint {
|
||||
->withStatistics();
|
||||
}
|
||||
|
||||
if($newsletter->status === Newsletter::STATUS_SENT ||
|
||||
$newsletter->status === Newsletter::STATUS_SENDING
|
||||
) {
|
||||
$queue = SendingQueue::where('newsletter_id', $newsletter->id)->findOne();
|
||||
}
|
||||
|
||||
// get preview url
|
||||
$newsletter->preview_url = NewsletterUrl::getViewInBrowserUrl($newsletter);
|
||||
$newsletter->preview_url = NewsletterUrl::getViewInBrowserUrl(
|
||||
$newsletter, $subscriber, $queue, $preview = true);
|
||||
|
||||
// convert object to array
|
||||
$listing_data['items'][$key] = $newsletter->asArray();
|
||||
|
@ -88,7 +88,6 @@ class Newsletter {
|
||||
);
|
||||
if($this->tracking_enabled) {
|
||||
$prepared_newsletter = NewsletterLinks::replaceSubscriberData(
|
||||
$newsletter['id'],
|
||||
$subscriber['id'],
|
||||
$queue['id'],
|
||||
$prepared_newsletter
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace MailPoet\Newsletter\Links;
|
||||
|
||||
use MailPoet\Models\Subscriber;
|
||||
use MailPoet\Router\Front as FrontRouter;
|
||||
use MailPoet\Router\Endpoints\Track as TrackEndpoint;
|
||||
use MailPoet\Models\NewsletterLink;
|
||||
@ -92,10 +93,10 @@ class Links {
|
||||
}
|
||||
|
||||
static function replaceSubscriberData(
|
||||
$newsletter_id,
|
||||
$subscriber_id,
|
||||
$queue_id,
|
||||
$content
|
||||
$content,
|
||||
$preview = false
|
||||
) {
|
||||
// match data tags
|
||||
$regex = sprintf(
|
||||
@ -103,6 +104,7 @@ class Links {
|
||||
preg_quote(self::DATA_TAG_CLICK),
|
||||
preg_quote(self::DATA_TAG_OPEN)
|
||||
);
|
||||
$subscriber = Subscriber::findOne($subscriber_id);
|
||||
preg_match_all($regex, $content, $matches);
|
||||
foreach($matches[1] as $index => $match) {
|
||||
$hash = null;
|
||||
@ -110,10 +112,11 @@ class Links {
|
||||
list(, $hash) = explode('-', $match);
|
||||
}
|
||||
$data = array(
|
||||
'newsletter' => $newsletter_id,
|
||||
'subscriber' => $subscriber_id,
|
||||
'queue' => $queue_id,
|
||||
'hash' => $hash
|
||||
'subscriber_id' => $subscriber->id,
|
||||
'subscriber_token' => Subscriber::generateToken($subscriber->email),
|
||||
'queue_id' => $queue_id,
|
||||
'link_hash' => $hash,
|
||||
'preview' => $preview
|
||||
);
|
||||
$router_action = ($matches[2][$index] === self::DATA_TAG_CLICK) ?
|
||||
TrackEndpoint::ACTION_CLICK :
|
||||
|
@ -30,18 +30,19 @@ class Url {
|
||||
$queue = ($queue) ? $queue->asArray() : false;
|
||||
}
|
||||
$data = array(
|
||||
'newsletter' => (!empty($newsletter['id'])) ?
|
||||
'newsletter_id' => (!empty($newsletter['id'])) ?
|
||||
$newsletter['id'] :
|
||||
$newsletter,
|
||||
'subscriber' => (!empty($subscriber['id'])) ?
|
||||
'subscriber_id' => (!empty($subscriber['id'])) ?
|
||||
$subscriber['id'] :
|
||||
$subscriber,
|
||||
'subscriber_token' => (!empty($subscriber['id'])) ?
|
||||
Subscriber::generateToken($subscriber['email']) :
|
||||
false,
|
||||
'queue' => (!empty($queue['id'])) ?
|
||||
'queue_id' => (!empty($queue['id'])) ?
|
||||
$queue['id'] :
|
||||
$queue
|
||||
$queue,
|
||||
'preview' => $preview
|
||||
);
|
||||
return FrontRouter::buildRequest(
|
||||
ViewInBrowserEndpoint::ENDPOINT,
|
||||
|
@ -12,44 +12,78 @@ use MailPoet\Newsletter\Shortcodes\Shortcodes;
|
||||
class ViewInBrowser {
|
||||
public $data;
|
||||
|
||||
function __construct($data) {
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
function view($data = false) {
|
||||
$data = ($data) ? $data : $this->data;
|
||||
$newsletter = ($data['newsletter'] !== false) ?
|
||||
Newsletter::findOne($data['newsletter']) :
|
||||
false;
|
||||
if(!$newsletter) $this->abort();
|
||||
$subscriber = ($data['subscriber'] !== false) ?
|
||||
$this->verifySubscriber($data['subscriber'], $data['subscriber_token']) :
|
||||
false;
|
||||
$queue = ($data['queue'] !== false) ?
|
||||
SendingQueue::findOne($data['queue']) :
|
||||
false;
|
||||
static function view($data) {
|
||||
$data = self::processData($data);
|
||||
if(!$data) return false;
|
||||
$rendered_newsletter =
|
||||
$this->getAndRenderNewsletter($newsletter, $subscriber, $queue);
|
||||
self::getAndRenderNewsletter(
|
||||
$data['newsletter'],
|
||||
$data['subscriber'],
|
||||
$data['queue'],
|
||||
$data['preview']
|
||||
);
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
echo $rendered_newsletter;
|
||||
exit;
|
||||
}
|
||||
|
||||
function verifySubscriber($subscriber_id, $subscriber_token) {
|
||||
$subscriber = Subscriber::findOne($subscriber_id);
|
||||
if(!$subscriber ||
|
||||
!Subscriber::verifyToken($subscriber->email, $subscriber_token)
|
||||
static function processData($data) {
|
||||
if(empty($data['subscriber_id']) ||
|
||||
empty($data['subscriber_token']) ||
|
||||
empty($data['newsletter_id'])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return $subscriber;
|
||||
$data['newsletter'] = self::getNewsletter($data['newsletter_id']);
|
||||
$data['subscriber'] = self::getSubscriber($data['subscriber_id']);
|
||||
$data['queue'] = self::getQueue($data['queue_id']);
|
||||
$data_processed_successfully =
|
||||
($data['subscriber'] && $data['newsletter']);
|
||||
return ($data_processed_successfully) ?
|
||||
self::validateData($data) :
|
||||
false;
|
||||
}
|
||||
|
||||
function getAndRenderNewsletter($newsletter, $subscriber, $queue) {
|
||||
if($queue) {
|
||||
$newsletter_body = json_decode($queue->newsletter_rendered_body, true);
|
||||
static function validateData($data) {
|
||||
if(!$data['subscriber']) return false;
|
||||
$subscriber_token_match =
|
||||
Subscriber::verifyToken($data['subscriber']['email'], $data['subscriber_token']);
|
||||
// return if this is an administrator user previewing the newsletter
|
||||
if($data['subscriber']['wp_user_id'] && $subscriber_token_match && $data['preview']) {
|
||||
return ($subscriber_token_match) ? $data : false;
|
||||
}
|
||||
// if queue exists, check if the newsletter was sent to the subscriber
|
||||
if($data['queue'] && $data['subscriber']) {
|
||||
$is_valid_subscriber =
|
||||
(!empty($data['queue']['subscribers']['processed']) &&
|
||||
in_array($data['subscriber']['id'], $data['queue']['subscribers']['processed']));
|
||||
$data = ($is_valid_subscriber && $subscriber_token_match) ? $data : false;
|
||||
} else {
|
||||
$renderer = new Renderer($newsletter->asArray(), $preview = true);
|
||||
$data = ($subscriber_token_match) ? $data : false;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
static function getNewsletter($newsletter_id) {
|
||||
$newsletter = Newsletter::findOne($newsletter_id);
|
||||
return ($newsletter) ? $newsletter->asArray() : $newsletter;
|
||||
}
|
||||
|
||||
static function getQueue($queue_id) {
|
||||
$queue = SendingQueue::findOne($queue_id);
|
||||
return ($queue) ? $queue->asArray() : $queue;
|
||||
}
|
||||
|
||||
static function getSubscriber($subscriber_id) {
|
||||
$subscriber = Subscriber::findOne($subscriber_id);
|
||||
return ($subscriber) ? $subscriber->asArray() : $subscriber;
|
||||
}
|
||||
|
||||
static function getAndRenderNewsletter($newsletter, $subscriber, $queue, $preview) {
|
||||
if($queue && $queue['newsletter_rendered_body']) {
|
||||
$newsletter_body = json_decode($queue['newsletter_rendered_body'], true);
|
||||
} else {
|
||||
$renderer = new Renderer($newsletter, $preview);
|
||||
$newsletter_body = $renderer->render();
|
||||
}
|
||||
$shortcodes = new Shortcodes(
|
||||
@ -60,16 +94,16 @@ class ViewInBrowser {
|
||||
$rendered_newsletter = $shortcodes->replace($newsletter_body['html']);
|
||||
if($queue && (boolean)Setting::getValue('tracking.enabled')) {
|
||||
$rendered_newsletter = Links::replaceSubscriberData(
|
||||
$newsletter->id,
|
||||
$subscriber->id,
|
||||
$queue->id,
|
||||
$rendered_newsletter
|
||||
$subscriber['id'],
|
||||
$queue['id'],
|
||||
$rendered_newsletter,
|
||||
$preview
|
||||
);
|
||||
}
|
||||
return $rendered_newsletter;
|
||||
}
|
||||
|
||||
private function abort() {
|
||||
private static function abort() {
|
||||
status_header(404);
|
||||
exit;
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
<?php
|
||||
namespace MailPoet\Router\Endpoints;
|
||||
|
||||
use MailPoet\Models\Newsletter;
|
||||
use MailPoet\Models\NewsletterLink;
|
||||
use MailPoet\Models\SendingQueue;
|
||||
use MailPoet\Models\Subscriber;
|
||||
use MailPoet\Statistics\Track\Clicks;
|
||||
use MailPoet\Statistics\Track\Opens;
|
||||
|
||||
@ -12,12 +16,68 @@ class Track {
|
||||
const ACTION_OPEN = 'open';
|
||||
|
||||
static function click($data) {
|
||||
$clicks = new Clicks($data);
|
||||
$clicks->track();
|
||||
Clicks::track(self::_processTrackData($data));
|
||||
}
|
||||
|
||||
static function open($data) {
|
||||
$opens = new Opens($data);
|
||||
$opens->track();
|
||||
Opens::track(self::_processTrackData($data));
|
||||
}
|
||||
|
||||
static function _processTrackData($data) {
|
||||
if(empty($data['queue_id']) ||
|
||||
empty($data['subscriber_id']) ||
|
||||
empty($data['subscriber_token'])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
$data['queue'] = self::_getQueue($data['queue_id']);
|
||||
$data['subscriber'] = self::_getSubscriber($data['subscriber_id']);
|
||||
$data['newsletter'] = (!empty($data['queue']['newsletter_id'])) ?
|
||||
self::_getNewsletter($data['queue']['newsletter_id']) :
|
||||
false;
|
||||
if(!empty($data['link_hash'])) {
|
||||
$data['link'] = self::_getLink($data['link_hash']);
|
||||
}
|
||||
$data_processed_successfully =
|
||||
($data['queue'] && $data['subscriber'] && $data['newsletter']);
|
||||
return ($data_processed_successfully) ?
|
||||
self::_validateTrackData($data) :
|
||||
false;
|
||||
}
|
||||
|
||||
static function _validateTrackData($data) {
|
||||
if(!$data['subscriber']) return false;
|
||||
$subscriber_token_match =
|
||||
Subscriber::verifyToken($data['subscriber']['email'], $data['subscriber_token']);
|
||||
// return if this is an administrator user previewing the newsletter
|
||||
if($data['subscriber']['wp_user_id'] && $data['preview']) {
|
||||
return ($subscriber_token_match) ? $data : false;
|
||||
}
|
||||
// check if the newsletter was sent to the subscriber
|
||||
$is_valid_subscriber =
|
||||
(!empty($data['queue']['subscribers']['processed']) &&
|
||||
in_array($data['subscriber']['id'], $data['queue']['subscribers']['processed']));
|
||||
return ($is_valid_subscriber && $subscriber_token_match) ? $data : false;
|
||||
}
|
||||
|
||||
static function _getNewsletter($newsletter_id) {
|
||||
$newsletter = Newsletter::findOne($newsletter_id);
|
||||
return ($newsletter) ? $newsletter->asArray() : $newsletter;
|
||||
}
|
||||
|
||||
static function _getSubscriber($subscriber_id) {
|
||||
$subscriber = Subscriber::findOne($subscriber_id);
|
||||
return ($subscriber) ? $subscriber->asArray() : $subscriber;
|
||||
}
|
||||
|
||||
static function _getQueue($queue_id) {
|
||||
$queue = SendingQueue::findOne($queue_id);
|
||||
return ($queue) ? $queue->asArray() : $queue;
|
||||
}
|
||||
|
||||
static function _getLink($link_hash) {
|
||||
$link = NewsletterLink::where('hash', $link_hash)
|
||||
->findOne();
|
||||
return ($link) ? $link->asArray() : $link;
|
||||
}
|
||||
}
|
@ -10,7 +10,6 @@ class ViewInBrowser {
|
||||
const ACTION_VIEW = 'view';
|
||||
|
||||
static function view($data) {
|
||||
$viewer = new NewsletterViewInBrowser($data);
|
||||
$viewer->view();
|
||||
NewsletterViewInBrowser::view($data);
|
||||
}
|
||||
}
|
@ -1,44 +1,21 @@
|
||||
<?php
|
||||
namespace MailPoet\Statistics\Track;
|
||||
|
||||
use MailPoet\Models\Newsletter;
|
||||
use MailPoet\Models\NewsletterLink;
|
||||
use MailPoet\Models\SendingQueue;
|
||||
use MailPoet\Models\StatisticsClicks;
|
||||
use MailPoet\Models\Subscriber;
|
||||
use MailPoet\Newsletter\Shortcodes\Categories\Link;
|
||||
|
||||
if(!defined('ABSPATH')) exit;
|
||||
|
||||
class Clicks {
|
||||
public $data;
|
||||
|
||||
function __construct($data) {
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
function track($data = false) {
|
||||
$data = ($data) ? $data : $this->data;
|
||||
$newsletter = $this->getNewsletter($data['newsletter']);
|
||||
$queue = $this->getQueue($data['queue']);
|
||||
// verify if queue belongs to the newsletter
|
||||
if($newsletter && $queue) {
|
||||
$queue = ($queue['newsletter_id'] === $newsletter['id']) ?
|
||||
$queue :
|
||||
false;
|
||||
}
|
||||
$subscriber = $this->getSubscriber($data['subscriber']);
|
||||
// verify if subscriber belongs to the queue
|
||||
if($queue && $subscriber) {
|
||||
// check if this newsletter was sent to
|
||||
$subscriber = (in_array($subscriber['id'], $queue['subscribers']['processed'])) ?
|
||||
$subscriber :
|
||||
false;
|
||||
}
|
||||
$link = $this->getLink($data['hash']);
|
||||
if(!$subscriber || !$newsletter || !$link || !$queue) {
|
||||
$this->abort();
|
||||
}
|
||||
static function track($data) {
|
||||
if(!$data || empty($data['link'])) return false;
|
||||
$subscriber = $data['subscriber'];
|
||||
$queue = $data['queue'];
|
||||
$newsletter = $data['newsletter'];
|
||||
$link = $data['link'];
|
||||
// log statistics only if the action did not come from
|
||||
// an admin user previewing the newsletter
|
||||
if(!$data['preview'] && !$subscriber['wp_user_id']) {
|
||||
$statistics = StatisticsClicks::where('link_id', $link['id'])
|
||||
->where('subscriber_id', $subscriber['id'])
|
||||
->where('newsletter_id', $newsletter['id'])
|
||||
@ -46,7 +23,7 @@ class Clicks {
|
||||
->findOne();
|
||||
if(!$statistics) {
|
||||
// track open event in case it did not register
|
||||
$this->trackOpenEvent($data);
|
||||
self::trackOpenEvent($data);
|
||||
$statistics = StatisticsClicks::create();
|
||||
$statistics->newsletter_id = $newsletter['id'];
|
||||
$statistics->link_id = $link['id'];
|
||||
@ -58,34 +35,14 @@ class Clicks {
|
||||
$statistics->count++;
|
||||
$statistics->save();
|
||||
}
|
||||
$url = $this->processUrl($link['url'], $newsletter, $subscriber, $queue);
|
||||
$this->redirectToUrl($url);
|
||||
}
|
||||
$url = self::processUrl($link['url'], $newsletter, $subscriber, $queue);
|
||||
self::redirectToUrl($url);
|
||||
}
|
||||
|
||||
function getNewsletter($newsletter_id) {
|
||||
$newsletter = Newsletter::findOne($newsletter_id);
|
||||
return ($newsletter) ? $newsletter->asArray() : $newsletter;
|
||||
}
|
||||
|
||||
function getSubscriber($subscriber_id) {
|
||||
$subscriber = Subscriber::findOne($subscriber_id);
|
||||
return ($subscriber) ? $subscriber->asArray() : $subscriber;
|
||||
}
|
||||
|
||||
function getQueue($queue_id) {
|
||||
$queue = SendingQueue::findOne($queue_id);
|
||||
return ($queue) ? $queue->asArray() : $queue;
|
||||
}
|
||||
|
||||
function getLink($hash) {
|
||||
$link = NewsletterLink::where('hash', $hash)
|
||||
->findOne();
|
||||
return ($link) ? $link->asArray() : $link;
|
||||
}
|
||||
|
||||
function processUrl($url, $newsletter, $subscriber, $queue) {
|
||||
static function processUrl($url, $newsletter, $subscriber, $queue) {
|
||||
if(preg_match('/\[link:(?P<action>.*?)\]/', $url, $shortcode)) {
|
||||
if(!$shortcode['action']) $this->abort();
|
||||
if(!$shortcode['action']) self::abort();
|
||||
$url = Link::processShortcodeAction(
|
||||
$shortcode['action'],
|
||||
$newsletter,
|
||||
@ -96,17 +53,16 @@ class Clicks {
|
||||
return $url;
|
||||
}
|
||||
|
||||
function trackOpenEvent($data) {
|
||||
$open = new Opens($data, $display_image = false);
|
||||
return $open->track();
|
||||
static function trackOpenEvent($data) {
|
||||
return Opens::track($data, $display_image = false);
|
||||
}
|
||||
|
||||
function abort() {
|
||||
static function abort() {
|
||||
status_header(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
function redirectToUrl($url) {
|
||||
static function redirectToUrl($url) {
|
||||
header('Location: ' . $url, true, 302);
|
||||
exit;
|
||||
}
|
||||
|
@ -1,76 +1,37 @@
|
||||
<?php
|
||||
namespace MailPoet\Statistics\Track;
|
||||
|
||||
use MailPoet\Models\Newsletter;
|
||||
use MailPoet\Models\SendingQueue;
|
||||
use MailPoet\Models\StatisticsOpens;
|
||||
use MailPoet\Models\Subscriber;
|
||||
|
||||
if(!defined('ABSPATH')) exit;
|
||||
|
||||
class Opens {
|
||||
public $data;
|
||||
public $return_image;
|
||||
|
||||
function __construct($data, $return_image = true) {
|
||||
$this->data = $data;
|
||||
$this->return_image = $return_image;
|
||||
}
|
||||
|
||||
function track($data = false) {
|
||||
$data = ($data) ? $data : $this->data;
|
||||
$newsletter = $this->getNewsletter($data['newsletter']);
|
||||
$queue = $this->getQueue($data['queue']);
|
||||
// verify if queue belongs to the newsletter
|
||||
if($newsletter && $queue) {
|
||||
$queue = ($queue['newsletter_id'] === $newsletter['id']) ?
|
||||
$queue :
|
||||
false;
|
||||
}
|
||||
$subscriber = $this->getSubscriber($data['subscriber']);
|
||||
// verify if subscriber belongs to the queue
|
||||
if($queue && $subscriber) {
|
||||
if(empty($queue['']))
|
||||
$subscriber = (in_array($subscriber['id'], $queue['subscribers']['processed'])) ?
|
||||
$subscriber :
|
||||
false;
|
||||
}
|
||||
if(!$subscriber || !$newsletter || !$queue) {
|
||||
return false;
|
||||
}
|
||||
$statistics = StatisticsOpens::where('subscriber_id', $data['subscriber'])
|
||||
->where('newsletter_id', $data['newsletter'])
|
||||
->where('queue_id', $data['queue'])
|
||||
static function track($data, $display_image = true) {
|
||||
if(!$data) return self::displayImage();
|
||||
$subscriber = $data['subscriber'];
|
||||
$queue = $data['queue'];
|
||||
$newsletter = $data['newsletter'];
|
||||
// log statistics only if the action did not come from
|
||||
// an admin user previewing the newsletter
|
||||
if (!$data['preview'] && !$subscriber['wp_user_id']) {
|
||||
$statistics = StatisticsOpens::where('subscriber_id', $subscriber['id'])
|
||||
->where('newsletter_id', $newsletter['id'])
|
||||
->where('queue_id', $queue['id'])
|
||||
->findOne();
|
||||
if(!$statistics) {
|
||||
$statistics = StatisticsOpens::create();
|
||||
$statistics->newsletter_id = $data['newsletter'];
|
||||
$statistics->subscriber_id = $data['subscriber'];
|
||||
$statistics->queue_id = $data['queue'];
|
||||
$statistics->newsletter_id = $newsletter['id'];
|
||||
$statistics->subscriber_id = $subscriber['id'];
|
||||
$statistics->queue_id = $queue['id'];
|
||||
$statistics->save();
|
||||
}
|
||||
if($this->return_image) {
|
||||
$this->returnImage();
|
||||
}
|
||||
return true;
|
||||
return ($display_image) ?
|
||||
self::displayImage() :
|
||||
true;
|
||||
}
|
||||
|
||||
function getNewsletter($newsletter_id) {
|
||||
$newsletter = Newsletter::findOne($newsletter_id);
|
||||
return ($newsletter) ? $newsletter->asArray() : $newsletter;
|
||||
}
|
||||
|
||||
function getSubscriber($subscriber_id) {
|
||||
$subscriber = Subscriber::findOne($subscriber_id);
|
||||
return ($subscriber) ? $subscriber->asArray() : $subscriber;
|
||||
}
|
||||
|
||||
function getQueue($queue_id) {
|
||||
$queue = SendingQueue::findOne($queue_id);
|
||||
return ($queue) ? $queue->asArray() : $queue;
|
||||
}
|
||||
|
||||
function returnImage() {
|
||||
static function displayImage() {
|
||||
// return 1x1 pixel transparent gif image
|
||||
header('Content-Type: image/gif');
|
||||
echo "\x47\x49\x46\x38\x37\x61\x1\x0\x1\x0\x80\x0\x0\xfc\x6a\x6c\x0\x0\x0\x2c\x0\x0\x0\x0\x1\x0\x1\x0\x0\x2\x2\x44\x1\x0\x3b";
|
||||
|
Reference in New Issue
Block a user