├── .gitignore
├── .htaccess
├── README.md
├── accounts.jpg
├── accounts.php
├── ajax
├── addAccount.php
├── checkAccount.php
├── delAccount.php
├── delRules.php
├── disapproveAppeal.php
├── editAccount.php
├── getAccountStatistics.php
├── payUnsettled.php
├── policyAppeal.php
├── startAd.php
├── stopAd.php
└── uploadRules.php
├── checkpassword.php
├── classes
├── FbAccount.php
├── FbAccountSerializer.php
├── FbRequests.php
├── RemaskProxy.php
└── ResponseFormatter.php
├── copyright.php
├── index.php
├── menu.php
├── scripts
├── accounts.js
├── actions.js
├── ad.js
├── adaccount.js
├── constants.js
├── index.js
├── mathhelpers.js
├── requests.js
├── statistics.js
└── tableformatter.js
├── settings.php
├── statistics.jpg
├── styles
├── bootstrap.min.css
├── bootstrap.min.js
├── img
│ ├── background.jpg
│ └── favicon.png
└── signin.css
└── version.php
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode
3 |
4 | ### VisualStudioCode ###
5 | .vscode/*
6 | !.vscode/settings.json
7 | !.vscode/tasks.json
8 | !.vscode/launch.json
9 | !.vscode/extensions.json
10 | *.code-workspace
11 |
12 | ### VisualStudioCode Patch ###
13 | # Ignore all local history of files
14 | .history
15 |
16 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
17 | /.vs
18 | /Remask.phpproj.user
19 | /.idea/modules.xml
20 | /.idea/ReMask.iml
21 | /.idea/vcs.xml
22 | /.idea/.gitignore
23 |
--------------------------------------------------------------------------------
/.htaccess:
--------------------------------------------------------------------------------
1 |
2 | Order allow,deny
3 | Deny from all
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ReMask
2 | 
3 | 
4 | Lightweight tool to check statistics of multiple Facebook ad accounts at once.
5 | Can show stats for:
6 | - single account or all accounts
7 | - only active accounts or all
8 | - various date ranges: today, yesterday and so on
9 |
10 | For Ad Account the following is displayed:
11 | - Account Name and Id
12 | - Spend limit, Current billing and Unbilled spend
13 | - Account Status and Disable Reason
14 | - Pixel Id
15 | - Total Statistics for all ads
16 |
17 | For each Ad in the account the following is displayed:
18 | - Creative
19 | - Name with the Link
20 | - Status with Review Feedback
21 | - Results count
22 |
23 | The following Actions are allowed:
24 | - Start/Stop ads;
25 | - Download/Upload autorules from/to ad accounts;
26 | - Send DISAPPROVE appeals
27 | - Send POLICY appeals
28 | - Pay UNSETTLED accounts
29 |
30 | # Requirements
31 | PHP 8.2 or newer
32 | # Installation
33 | Copy-Paste all the files to the ROOT folder of your domain, open settings.php and edit your password there.
34 |
35 | Then go to http://yourdomain.com?password=yourpassword and enjoy!
36 | ## Donate
37 | If you like this software, please donate!
38 |
39 | **USDT TRC20** TKeNEVndhPSKXuYmpEwF4fVtWUvfCnWmra
40 | ## Code
41 | https://t.me/yellow_web
42 | ## Design
43 | https://t.me/bearded_cpa
44 | ## Original idea
45 | https://t.me/tron_cpa
46 |
47 | https://t.me/adamusfb
48 |
49 |
--------------------------------------------------------------------------------
/accounts.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dvygolov/ReMask/66a402344ab328da185be1898658c0b39699dd37/accounts.jpg
--------------------------------------------------------------------------------
/accounts.php:
--------------------------------------------------------------------------------
1 | deserialize();
12 | ?>
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | = $accounts[$i]->name ?> |
59 |
60 | [Edit]
61 | |
62 |
63 | [Delete]
64 | |
65 |
66 |
67 |
68 |
69 |
70 |
71 | Add Account
72 |
73 |
74 |
95 |
96 | = include 'copyright.php' ?>
97 |
98 |
--------------------------------------------------------------------------------
/ajax/addAccount.php:
--------------------------------------------------------------------------------
1 | addOrUpdateAccount($newAcc);
14 | ResponseFormatter::Respond(['res'=>json_encode($newAcc)]);
15 | } else {
16 | ResponseFormatter::Respond(['error'=>"Name,Token or Cookies not set!"]);
17 | }
--------------------------------------------------------------------------------
/ajax/checkAccount.php:
--------------------------------------------------------------------------------
1 | ApiGet($acc,"me");
16 | ResponseFormatter::Respond($resp);
--------------------------------------------------------------------------------
/ajax/delAccount.php:
--------------------------------------------------------------------------------
1 | deleteAccountByName($_GET["name"]);
12 | http_response_code(200);
13 | } else
14 | http_response_code(500);
15 |
--------------------------------------------------------------------------------
/ajax/delRules.php:
--------------------------------------------------------------------------------
1 | getAccountByName($_POST['acc_name']);
15 | if ($acc == null) die("No account with name " . $_POST['acc_name'] . " found!");
16 | $rules = explode(",", $_POST['rules']);
17 | $batch = [];
18 | foreach ($rules as $rule) {
19 | $batchItem = [
20 | 'name' => $rule,
21 | 'method' => 'DELETE',
22 | 'relative_url' => $rule
23 | ];
24 | $batch[] = $batchItem;
25 | }
26 |
27 | $batchJson = urlencode(json_encode($batch));
28 |
29 | $req = new FbRequests();
30 | $resp = $req->ApiPost($acc, "", "batch=$batchJson&include_headers=false");
31 | if (!$resp['error']) {
32 | $batchJson = json_decode($resp['res'], true);
33 | foreach ($batchJson as $batchItem){
34 | if ($batchItem['code']!=200) {
35 | $resp['res']='';
36 | $resp['error'] = $batchItem['body'];
37 | break;
38 | }
39 | }
40 | }
41 |
42 | ResponseFormatter::Respond($resp);
43 |
--------------------------------------------------------------------------------
/ajax/disapproveAppeal.php:
--------------------------------------------------------------------------------
1 | getAccountByName($_POST['acc_name']);
15 |
16 | $req = new FbRequests();
17 | $accId = $_POST['accid'];
18 | $adId = $_POST['adid'];
19 | $dtsg = $req->GetDtsg($acc);
20 | $url = "https://business.facebook.com/ads/integrity/appeals_case/creation/ajax/";
21 | $body = "ad_account_id=$accId&adgroup_ids[0]=$adId&callsite=ACCOUNT_QUALITY&__a=1&fb_dtsg=". urlencode($dtsg);
22 | $res = $req->PrivateApiPost($acc, $body, $url);
23 |
24 | $responseArray = json_decode($res['res'], true);
25 | $appealSent = null;
26 | foreach ($responseArray['payload']['adgroupIDToSuccess']['__map'] as $map) {
27 | if ($map[0] == $adId) {
28 | $appealSent = $map[1];
29 | break;
30 | }
31 | }
32 |
33 | if ($appealSent === null) {
34 | $res['res'] = null;
35 | $res['error'] = "Ad ID not found in the response.";
36 | } else if (!$appealSent){
37 | $res['res'] = null;
38 | $res['error'] = "Appeal NOT Sent!";
39 | }
40 | ResponseFormatter::Respond($res);
41 |
--------------------------------------------------------------------------------
/ajax/editAccount.php:
--------------------------------------------------------------------------------
1 | "Name not set!"]);
12 | exit;
13 | }
14 | $serializer = new FbAccountSerializer(ACCOUNTSFILENAME);
15 | $acc = $serializer->getAccountByName($_GET["name"]);
16 | ResponseFormatter::Respond(['res' => json_encode($acc)]);
17 |
--------------------------------------------------------------------------------
/ajax/getAccountStatistics.php:
--------------------------------------------------------------------------------
1 | getAccountByName($_GET['acc_name']);
12 | if ($acc == null) die("No account with name " . $_GET['acc_name'] . " found!");
13 | $datetime = $_GET['datetime'];
14 | $requestParams = "fields=
15 | name,
16 | status,
17 | account_status,
18 | disable_reason,
19 | adspixels{id},
20 | promote_pages{access_token,id},
21 | business{name,link},
22 | all_payment_methods{pm_credit_card{account_id,credential_id,display_string,exp_month,exp_year}},
23 | funding_source_details,
24 | adrules_library{id,name,evaluation_spec,execution_spec,schedule_spec},
25 | spend_cap,
26 | current_unbilled_spend,
27 | balance,
28 | amount_spent,
29 | adtrust_dsl,
30 | adspaymentcycle,
31 | currency,
32 | timezone_name,
33 | created_time,
34 | ads.date_preset($datetime).time_increment($datetime).limit(500){
35 | id,
36 | name,
37 | impressions,
38 | clicks,
39 | link_ctr,
40 | spent,
41 | insights.limit(500).date_preset($datetime){
42 | conversion_rate_ranking,
43 | engagement_rate_ranking,
44 | quality_ranking,
45 | results{values{value}},
46 | inline_link_click_ctr,
47 | inline_link_clicks,
48 | ctr,
49 | cpc,
50 | cpm
51 | },
52 | creative{
53 | effective_object_story_id,
54 | effective_instagram_story_id,
55 | actor_id
56 | },
57 | adlabels,
58 | created_time,
59 | recommendations,
60 | updated_time,
61 | ad_review_feedback,
62 | bid_info,
63 | delivery_info,
64 | status,
65 | effective_status,
66 | adcreatives.limit(500){
67 | place_page_set_id,
68 | object_story_spec{
69 | instagram_actor_id,
70 | link_data{link},
71 | page_id
72 | },
73 | url_tags,
74 | image_crops,
75 | image_url,
76 | status,
77 | thumbnail_url
78 | }
79 | }";
80 |
81 | $requestParams = preg_replace('/\s+/', '', $requestParams);
82 | $req = new FbRequests();
83 | $resp = $req->ApiGet($acc, "me/adaccounts?$requestParams");
84 | $stats = json_decode($resp['res'], true);
85 | if (ResponseFormatter::ResponseHasError($resp))
86 | {
87 | ResponseFormatter::Respond($resp);
88 | exit;
89 | }
90 |
91 |
92 | $batch = [];
93 | foreach ($stats['data'] as $adAcc) {
94 | $batchItem = [
95 | 'name' => $adAcc['id'],
96 | 'method' => 'GET',
97 | 'relative_url' =>
98 | $adAcc['id'] . "/ads?fields=id,insights.limit(500).date_preset($datetime){results,cost_per_result}"
99 | ];
100 | $batch[] = $batchItem;
101 | }
102 |
103 | $batchJson = urlencode(json_encode($batch));
104 |
105 | $req = new FbRequests();
106 | $resp = $req->ApiPost($acc, "", "batch=$batchJson&include_headers=false");
107 | $results = json_decode($resp['res'], true);
108 |
109 | $adResults = [];
110 | foreach ($results as $adAccResult) {
111 | $jsonAdAccResult = json_decode($adAccResult['body'], true);
112 | foreach ($jsonAdAccResult['data'] as $adResult) {
113 | $results = $adResult['insights']['data'][0]['results'][0];
114 | $costs = $adResult['insights']['data'][0]['cost_per_result'][0];
115 | $adResults[$adResult['id']] = [
116 | 'results' => isset($results['values'])?$results['values'][0]['value'] : 0,
117 | 'cost_per_result' => isset($costs['values'])?$costs['values'][0]['value'] : 0
118 | ];
119 | }
120 | }
121 |
122 | $finalRes = [];
123 | $finalRes['stats'] = $stats;
124 | $finalRes['insights'] = $adResults;
125 | $resp['res'] = json_encode($finalRes);
126 | ResponseFormatter::Respond($resp);
--------------------------------------------------------------------------------
/ajax/payUnsettled.php:
--------------------------------------------------------------------------------
1 | getAccountByName($accName);
13 | if ($acc == null) die("No account with name $accName found!");
14 |
15 | $accId = $_POST['accid'];
16 | $paymentId = $_POST['paymentid'];
17 | $sum = $_POST['sum'];
18 | $currency = $_POST['currency'];
19 |
20 | $ijson = array();
21 | $input = array();
22 | $input['client_mutation_id'] = "1";
23 | $input['actor_id'] = $acc->userId;
24 | $input['billable_account_payment_legacy_account_id'] = $accId;
25 | $input['credential_id'] = $paymentId;
26 | $paymentAmount = array();
27 | $paymentAmount['amount'] = str_replace(',', '.', $sum);
28 | $paymentAmount['currency'] = $currency;
29 | $input['payment_amount'] = $paymentAmount;
30 | $input['transaction_initiation_source'] = "CUSTOMER";
31 | $ijson['input'] = $input;
32 | $vars = json_encode($ijson);
33 |
34 | $req = new FbRequests();
35 | $dtsg = $req->GetDtsg($acc);
36 | $resp = $req->PrivateApiPost($acc, "fb_dtsg=$dtsg&__a=1&doc_id=5553047091425712&variables=$vars","https://adsmanager.secure.facebook.com/ajax/payment/token_proxy.php?tpe=%2Fapi%2Fgraphql%2F&");
37 |
38 | ResponseFormatter::Respond($resp);
--------------------------------------------------------------------------------
/ajax/policyAppeal.php:
--------------------------------------------------------------------------------
1 | getAccountByName($_POST['acc_name']);
15 | $accId = $_POST['accid'];
16 |
17 | $req = new FbRequests();
18 |
19 | $variables = [
20 | "input" => [
21 | "client_mutation_id" => "1",
22 | "actor_id" => $acc->userId,
23 | "ad_account_id" => $accId,
24 | "ids_issue_ent_id" => "880164863403788",
25 | "appeal_comment" => "I'm not sure which policy was violated.",
26 | "callsite" => "ACCOUNT_QUALITY",
27 | ],
28 | ];
29 | $jsonVars = json_encode($variables);
30 |
31 | $dtsg = $req->GetDtsg($acc);
32 | $body = "__a=1&fb_dtsg=" . urlencode($dtsg) . "&variables=" . urlencode($jsonVars) . "&doc_id=5197966936890203";
33 | $resp = $req->PrivateApiPost($acc, $body);
34 | $js = json_decode($resp['res'], true);
35 | $policySent = ($js['data']['xfb_alr_ad_account_appeal_create']['success'] === true);
36 | if (!$policySent) {
37 | $resp['res'] = null;
38 | $resp['error'] = "Couldn't send Policy Ticket!";
39 | }
40 | ResponseFormatter::Respond($resp);
--------------------------------------------------------------------------------
/ajax/startAd.php:
--------------------------------------------------------------------------------
1 | getAccountByName($_POST['acc_name']);
15 | if ($acc == null) die("No account with name " . $_POST['acc_name'] . " found!");
16 |
17 | $adId = $_POST['adid'];
18 | $req = new FbRequests();
19 | $resp = $req->ApiGet($acc, "$adId?status=ACTIVE&method=post");
20 | ResponseFormatter::Respond($resp);
21 |
--------------------------------------------------------------------------------
/ajax/stopAd.php:
--------------------------------------------------------------------------------
1 | getAccountByName($_POST['acc_name']);
15 | if ($acc == null) die("No account with name " . $_POST['acc_name'] . " found!");
16 |
17 | $adId = $_POST['adid'];
18 | $req = new FbRequests();
19 | $resp = $req->ApiGet($acc, "$adId?status=PAUSED&method=post");
20 | ResponseFormatter::Respond($resp);
21 |
--------------------------------------------------------------------------------
/ajax/uploadRules.php:
--------------------------------------------------------------------------------
1 | getAccountByName($_POST['acc_name']);
15 | if ($acc == null) die("No account with name " . $_POST['acc_name'] . " found!");
16 | $rules = json_decode($_POST['rules'], true);
17 | $accId = $_POST['accid'];
18 | $batch = [];
19 | foreach ($rules as $rule) {
20 | $evspecstr = json_encode($rule['evaluation_spec']);
21 | $exspecstr = json_encode($rule['execution_spec']);
22 | $shedspecstr = json_encode($rule['schedule_spec']);
23 |
24 | $batchItem = [
25 | 'name' => $rule['name'],
26 | 'method' => 'POST',
27 | 'body' => "name={$rule['name']}&evaluation_spec={$evspecstr}&execution_spec={$exspecstr}&schedule_spec={$shedspecstr}&status=ENABLED",
28 | 'relative_url' => "act_{$accId}/adrules_library"
29 | ];
30 |
31 | $batch[] = $batchItem;
32 | }
33 |
34 | $batchJson = urlencode(json_encode($batch));
35 |
36 | $req = new FbRequests();
37 | $resp = $req->ApiPost($acc, "", "batch=$batchJson&include_headers=false");
38 | if (!$resp['error']) {
39 | $batchJson = json_decode($resp['res'], true);
40 | foreach ($batchJson as $batchItem){
41 | if ($batchItem['code']!=200) {
42 | $resp['res']='';
43 | $resp['error'] = json_decode($batchItem['body'],true)['error']['message'];
44 | break;
45 | }
46 | }
47 | }
48 |
49 | ResponseFormatter::Respond($resp);
--------------------------------------------------------------------------------
/checkpassword.php:
--------------------------------------------------------------------------------
1 | userId = $jsonCookies[array_search('c_user', array_column($jsonCookies, 'name'))]['value'] ?? '';
20 | $this->name = $name;
21 | $this->token = $token;
22 | $this->cookies = $jsonCookies;
23 | $this->dtsg = $dtsg;
24 | $this->proxy = $proxy;
25 | }
26 |
27 | public function toArray(): array
28 | {
29 | return [
30 | 'name' => $this->name,
31 | 'token' => $this->token,
32 | 'cookies' => $this->cookies,
33 | 'dtsg' => $this->dtsg,
34 | 'proxy' => $this->proxy?->toArray()
35 | ];
36 | }
37 |
38 | public static function fromArray($accountData): FbAccount
39 | {
40 | $proxy = is_string($accountData['proxy']) ?
41 | RemaskProxy::fromSemicolonString($accountData['proxy']) :
42 | RemaskProxy::fromArray($accountData['proxy']);
43 | if (is_string($accountData['cookies'])){
44 | $cookies = $accountData['cookies'];
45 | } else{
46 | $cookies= json_encode($accountData['cookies']);
47 | }
48 |
49 | return new FbAccount(
50 | $accountData['name'],
51 | $accountData['token'],
52 | $cookies,
53 | $accountData['dtsg'] ?? null,
54 | $proxy
55 | );
56 | }
57 |
58 | public function getCurlCookies(): string
59 | {
60 | $cookieString = '';
61 | foreach ($this->cookies as $cookie) {
62 | $cookieString .= "{$cookie['name']}={$cookie['value']}; ";
63 | }
64 | return rtrim($cookieString, '; ');
65 | }
66 | }
--------------------------------------------------------------------------------
/classes/FbAccountSerializer.php:
--------------------------------------------------------------------------------
1 | filename = $filename;
10 | }
11 |
12 | public function serialize(array $facebookAccounts): void
13 | {
14 | $accountsData = [];
15 | foreach ($facebookAccounts as $account) {
16 | $accountsData[] = $account->toArray();
17 | }
18 |
19 | $json = json_encode($accountsData, JSON_PRETTY_PRINT);
20 | file_put_contents($this->filename, $json);
21 | }
22 |
23 | public function deserialize(): array
24 | {
25 | if (!file_exists($this->filename)) return [];
26 | $json = file_get_contents($this->filename);
27 | $data = json_decode($json, true);
28 |
29 | $facebookAccounts = [];
30 |
31 | foreach ($data as $accountData) {
32 | $facebookAccounts[] = FbAccount::fromArray($accountData);
33 | }
34 |
35 | return $facebookAccounts;
36 | }
37 |
38 | public function addOrUpdateAccount(FbAccount $acc):void
39 | {
40 | $accounts = $this->deserialize();
41 | $accountExists = false;
42 | foreach ($accounts as &$account) {
43 | if ($account->name === $acc->name) {
44 | $accountExists = true;
45 | $account->token = $acc->token;
46 | $account->cookies = $acc->cookies;
47 | if (isset($acc->proxy)) {
48 | $account->proxy = $acc->proxy;
49 | }
50 | break;
51 | }
52 | }
53 |
54 | if (!$accountExists) {
55 | $accounts[] = $acc;
56 | }
57 |
58 | $this->serialize($accounts);
59 | }
60 |
61 | public function getAccountByName($name): ?FbAccount
62 | {
63 | $accounts = $this->deserialize();
64 | $filteredAccounts = array_filter($accounts, function ($account) use ($name) {
65 | return $account->name === $name;
66 | });
67 | return !empty($filteredAccounts) ? reset($filteredAccounts) : null;
68 | }
69 |
70 | public function deleteAccountByName($name): array
71 | {
72 | // Deserialize the accounts from the JSON file
73 | $facebookAccounts = $this->deserialize();
74 |
75 | // Remove the account with the matching name
76 | $facebookAccounts = array_filter($facebookAccounts, function ($account) use ($name) {
77 | return $account->name !== $name;
78 | });
79 |
80 | // Re-index the array
81 | $facebookAccounts = array_values($facebookAccounts);
82 |
83 | // Serialize the updated array of accounts back to the JSON file
84 | $this->serialize($facebookAccounts);
85 |
86 | // Return the updated array of accounts
87 | return $facebookAccounts;
88 | }
89 | }
--------------------------------------------------------------------------------
/classes/FbRequests.php:
--------------------------------------------------------------------------------
1 | api . $url . "&access_token=" . $acc->token :
20 | $this->api . $url . "?access_token=" . $acc->token;
21 | $optArray = array(
22 | CURLOPT_URL => $finalUrl,
23 | CURLOPT_RETURNTRANSFER => true,
24 | CURLOPT_COOKIE => $acc->getCurlCookies(),
25 | CURLOPT_HTTPHEADER => $headers,
26 | );
27 | return $this->Execute($acc, $optArray);
28 | }
29 |
30 | public function ApiPost(FbAccount $acc, string $url, string $body): array
31 | {
32 | $headers = [
33 | "content-type: application/x-www-form-urlencoded",
34 | "sec-ch-ua-mobile: ?0",
35 | 'sec-ch-ua-platform: "Windows"',
36 | "sec-fetch-dest: empty",
37 | "sec-fetch-mode: cors",
38 | "sec-fetch-site: same-origin",
39 | ];
40 | $finalUrl = $this->api . $url;
41 | $finalBody = $body . "&access_token=" . $acc->token;
42 |
43 | $optArray = array(
44 | CURLOPT_URL => $finalUrl,
45 | CURLOPT_RETURNTRANSFER => true,
46 | CURLOPT_POST => true, // Set the request method to POST
47 | CURLOPT_POSTFIELDS => $finalBody, // Set the POST data
48 | CURLOPT_COOKIE => $acc->getCurlCookies(),
49 | CURLOPT_HTTPHEADER => $headers,
50 | );
51 |
52 |
53 | return $this->Execute($acc, $optArray);
54 | }
55 |
56 | public function PrivateApiPost($acc, $body, $customUrl = null): array
57 | {
58 | $headers = [
59 | "content-type: application/x-www-form-urlencoded",
60 | "sec-ch-ua-mobile: ?0",
61 | 'sec-ch-ua-platform: "Windows"',
62 | "sec-fetch-dest: empty",
63 | "sec-fetch-mode: cors",
64 | "sec-fetch-site: same-origin",
65 | ];
66 |
67 | $url = $customUrl ?? "https://www.facebook.com/api/graphql";
68 | $optArray = array(
69 | CURLOPT_URL => $url,
70 | CURLOPT_RETURNTRANSFER => true,
71 | CURLOPT_POST => true, // Set the request method to POST
72 | CURLOPT_POSTFIELDS => $body, // Set the POST data
73 | CURLOPT_COOKIE => $acc->getCurlCookies(),
74 | CURLOPT_HTTPHEADER => $headers,
75 | );
76 |
77 | $res = $this->Execute($acc, $optArray);
78 | $res['res'] = str_replace("for (;;);", "", $res['res']);
79 | return $res;
80 | }
81 |
82 | public function GetDtsg(FbAccount $acc): ?string
83 | {
84 | $resp = $this->RawGet($acc, "https://mbasic.facebook.com/photos/upload");
85 | $pattern = '/name="fb_dtsg"\s+value="([^"]+)"/';
86 | if (preg_match($pattern, $resp["res"], $matches)) {
87 | return $matches[1];
88 | }
89 | return null;
90 | }
91 |
92 | private function GetNewToken(FbAccount $acc): ?string
93 | {
94 | $res = $this->RawGet($acc, "https://adsmanager.facebook.com/adsmanager/manage/all");
95 |
96 | $location = $this->GetLocationHeader($res['headers']);
97 | if (str_contains($location, 'checkpoint') ||
98 | str_contains($location, 'login')) return null;
99 |
100 | $redirect = null;
101 | if (preg_match('/window\.location\.replace\("([^"]+)/', $res['res'], $matches)) {
102 | $redirect = str_replace("\\", "", $matches[1]);
103 | }
104 | if ($redirect === null) {
105 | return $this->ParseToken($res['res']);
106 | } else {
107 | preg_match('/act=(\d+)/', $res['res'], $matches);
108 | $act = $matches[1];
109 | $finalUrl = "https://adsmanager.facebook.com/adsmanager?act={$act}&nav_source=no_referrer";
110 | $adsRes = $this->RawGet($acc, $finalUrl);
111 | return $this->ParseToken($adsRes['res']);
112 | }
113 | }
114 |
115 | private function ReplaceAccessToken($optArray, $newToken) {
116 | // Regular expression to match access_token= followed by any character sequence that isn't & or end of string
117 | $pattern = '/(access_token=)([^&]*)/';
118 |
119 | // Check if it's a POST request
120 | if (isset($optArray[CURLOPT_POST]) && $optArray[CURLOPT_POST]) {
121 | if (isset($optArray[CURLOPT_POSTFIELDS])) {
122 | $optArray[CURLOPT_POSTFIELDS] = preg_replace($pattern, '$1' . $newToken, $optArray[CURLOPT_POSTFIELDS]);
123 | }
124 | } else {
125 | // It's a GET request
126 | if (isset($optArray[CURLOPT_URL])) {
127 | $optArray[CURLOPT_URL] = preg_replace($pattern, '$1' . $newToken, $optArray[CURLOPT_URL]);
128 | }
129 | }
130 | return $optArray;
131 | }
132 |
133 |
134 | private function ParseToken($text)
135 | {
136 | if (preg_match('/EAAB[^"]+/', $text, $matches)) {
137 | return $matches[0];
138 | }
139 | return null;
140 | }
141 |
142 | private function GetLocationHeader(string $headers): ?string
143 | {
144 | preg_match("/location: (.*?)\n/", $headers, $matches);
145 | $header = isset($matches[1]) ? trim($matches[1]) : null;
146 | return $header;
147 | }
148 |
149 | private function RawGet(FbAccount $acc, string $url): array
150 | {
151 | $headers = [
152 | "sec-fetch-dest: document",
153 | "sec-fetch-mode: navigate",
154 | "sec-fetch-site: none",
155 | "sec-fetch-user: ?1"
156 | ];
157 | $optArray = array(
158 | CURLOPT_URL => $url,
159 | CURLOPT_RETURNTRANSFER => true,
160 | CURLOPT_COOKIE => $acc->getCurlCookies(),
161 | CURLOPT_HTTPHEADER => $headers,
162 | );
163 | $res = $this->Execute($acc, $optArray);
164 | return $res;
165 | }
166 |
167 | private function Execute(FbAccount $acc, array $optArray): array
168 | {
169 | $userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36";
170 | $optArray[CURLOPT_USERAGENT] = $userAgent;
171 | $optArray[CURLOPT_SSL_VERIFYPEER] = false;
172 | $optArray[CURLOPT_HEADER] = true;
173 |
174 | $acc->proxy?->AddToCurlOptions($optArray);
175 | $curl = curl_init();
176 | curl_setopt_array($curl, $optArray);
177 | $res = curl_exec($curl);
178 | $info = curl_getinfo($curl);
179 | $error = curl_error($curl);
180 | $headers = null;
181 | if ($res !== false) {
182 | $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
183 | $headers = substr($res, 0, $header_size);
184 | $res = substr($res, $header_size);
185 | }
186 | curl_close($curl);
187 | if (str_contains($res, "Error loading application")) {
188 | $token = $this->GetNewToken($acc);
189 | if ($token !== null) {
190 | $acc->token = $token;
191 | $serializer = new FbAccountSerializer(ACCOUNTSFILENAME);
192 | $serializer->addOrUpdateAccount($acc);
193 | $optArray = $this->ReplaceAccessToken($optArray, $token);
194 | return $this->Execute($acc, $optArray);
195 | }
196 | }
197 | return [
198 | "res" => $res,
199 | "headers" => $headers,
200 | "info" => $info,
201 | "error" => $error
202 | ];
203 | }
204 | }
--------------------------------------------------------------------------------
/classes/RemaskProxy.php:
--------------------------------------------------------------------------------
1 | $this->type,
16 | 'ip' => $this->ip,
17 | 'port' => $this->port,
18 | 'login' => $this->login,
19 | 'password' => $this->password
20 | ];
21 | }
22 |
23 | public static function fromSemicolonString($proxyString): ?RemaskProxy
24 | {
25 | if (!$proxyString) return null;
26 | $p = new RemaskProxy();
27 | $parts = explode(':', $proxyString);
28 | if (count($parts) === 5) {
29 | $p->type = $parts[0];
30 | $p->ip = $parts[1];
31 | $p->port = (int)$parts[2];
32 | $p->login = $parts[3];
33 | $p->password = $parts[4];
34 | return $p;
35 | } else {
36 | throw new InvalidArgumentException('Invalid proxy string format. Expected format: type:ip:port:login:password');
37 | }
38 | }
39 |
40 | public static function fromArray($proxyData): ?RemaskProxy
41 | {
42 | if (!$proxyData) {
43 | return null;
44 | }
45 | $p = new RemaskProxy();
46 | $p->type = $proxyData['type'];
47 | $p->ip = $proxyData['ip'];
48 | $p->port = $proxyData['port'];
49 | $p->login = $proxyData['login'];
50 | $p->password = $proxyData['password'];
51 | return $p;
52 | }
53 |
54 | public function AddToCurlOptions(array &$optArray): void
55 | {
56 | if (strtolower($this->type)==='http')
57 | $optArray[CURLOPT_PROXYTYPE] = 'HTTP';
58 | else
59 | $optArray[CURLOPT_PROXYTYPE] = CURLPROXY_SOCKS5;
60 | $optArray[CURLOPT_PROXY] = strtolower($this->type)==='http'?"http://".$this->ip:"socks5://".$this->ip;
61 | $optArray[CURLOPT_PROXYPORT] = $this->port;
62 | $optArray[CURLOPT_PROXYUSERPWD] = $this->login . ':' . $this->password;
63 | }
64 | }
--------------------------------------------------------------------------------
/classes/ResponseFormatter.php:
--------------------------------------------------------------------------------
1 | Жёлтый Веб | ©ТРОН | Scripts by Adam | Бородатый арбитраж
2 |
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | deserialize();
15 | ?>
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Statistics
33 |
34 |
35 |
41 |
45 |
52 |
53 |
57 |
58 |
59 |
60 |
63 |
64 |
65 | = include 'copyright.php' ?>
66 |
67 |
69 |
70 |
--------------------------------------------------------------------------------
/menu.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/accounts.js:
--------------------------------------------------------------------------------
1 | import {Requests} from './requests.js';
2 |
3 | window.addEventListener('DOMContentLoaded', () => {
4 | const delAccButton = document.querySelectorAll('.delaccount');
5 | delAccButton.forEach(button => {
6 | button.addEventListener('click', async (event) => {
7 | const socname = event.target.dataset.name;
8 | await delAccount(socname);
9 | });
10 | });
11 |
12 | const editAccButton = document.querySelectorAll('.editaccount');
13 | editAccButton.forEach(button => {
14 | button.addEventListener('click', async (event) => {
15 | const socname = event.target.dataset.name;
16 | await editAccount(socname);
17 | });
18 | });
19 |
20 |
21 | const loadingIcon = document.getElementById('loadingIcon');
22 | const addAccountButton = document.getElementById("addaccountbutton");
23 | addAccountButton.addEventListener('click', async (event) => {
24 | // Show the loading icon
25 | loadingIcon.style.display = 'inline-block';
26 |
27 | await addAccount();
28 | // Hide the loading icon
29 | loadingIcon.style.display = 'none';
30 | });
31 | });
32 |
33 | async function addAccount() {
34 | const name = document.add.name.value;
35 | const token = document.add.token.value;
36 | const cookies = document.add.cookies.value;
37 | const proxy = document.add.proxy.value;
38 | if (!await validate_form(name, token, cookies, proxy)) return;
39 | let resp = await Requests.post("ajax/addAccount.php",
40 | `name=${encodeURIComponent(name)}&token=${encodeURIComponent(token)}&cookies=${encodeURIComponent(cookies)}&proxy=${encodeURIComponent(proxy)}`
41 | );
42 | let checkResp = await Requests.checkResponse(resp, false);
43 | if (checkResp.success)
44 | window.location.reload();
45 | else alert(`Error adding account: ${checkResp.error}`);
46 | }
47 |
48 | async function editAccount(name) {
49 | let resp = await fetch(`ajax/editAccount.php?name=${name}`);
50 | let checkResp = await Requests.checkResponse(resp);
51 | if (checkResp.success) {
52 | document.add.name.value = checkResp.data.name;
53 | document.add.name.readOnly = true;
54 | document.add.token.value = checkResp.data.token;
55 | document.add.cookies.value = JSON.stringify(checkResp.data.cookies);
56 | // Convert the proxy JSON object to a colon-separated string
57 | const proxyObj = checkResp.data.proxy;
58 | const proxyStr = `${proxyObj.type}:${proxyObj.ip}:${proxyObj.port}:${proxyObj.login}:${proxyObj.password}`;
59 | document.add.proxy.value = proxyStr;
60 | } else alert(`Error editing account: ${checkResp.error}`);
61 | }
62 |
63 | async function delAccount(name) {
64 | let resp = await fetch(`ajax/delAccount.php?name=${name}`);
65 | let checkResp = await Requests.checkResponse(resp, false);
66 | if (checkResp.success)
67 | window.location.reload();
68 | else alert(`Error deleting account: ${checkResp.error}`);
69 | }
70 |
71 | async function validate_form(name, token, cookies, proxy) {
72 | if (name === "" || token === "" || cookies === "" || proxy === "") {
73 | alert("You need to fill ALL fields!");
74 | return false;
75 | }
76 |
77 | let cookiesParsed;
78 | try {
79 | cookiesParsed = JSON.parse(cookies);
80 | } catch (e) {
81 | alert("Cookies must be a valid JSON array!");
82 | return false;
83 | }
84 | if (!(Array.isArray(cookiesParsed))) {
85 | alert("Cookies must be a JSON array!");
86 | return false;
87 | }
88 |
89 | let hasCUserCookie = false;
90 | for (const cookie of cookiesParsed) {
91 | if (!cookie.hasOwnProperty("name") || !cookie.hasOwnProperty("value")) {
92 | alert("Each cookie in the JSON array must have a 'name' and a 'value' field!");
93 | return false;
94 | }
95 | if (cookie.name === "c_user") {
96 | hasCUserCookie = true;
97 | }
98 | }
99 |
100 | if (!hasCUserCookie) {
101 | alert("The JSON array must contain a cookie with the 'name' field set to 'c_user'!");
102 | return false;
103 | }
104 |
105 | if (!token.startsWith("EAAB")) {
106 | alert("Token must start with 'EAAB'!");
107 | return false;
108 | }
109 |
110 | const proxyElements = proxy.split(':');
111 | if (proxyElements.length !== 5) {
112 | alert("Proxy must be in the format 'type:ip:port:login:pass'!");
113 | return false;
114 | }
115 |
116 | const proxyType = proxyElements[0].toLowerCase();
117 | if (proxyType !== 'http' && proxyType !== 'socks') {
118 | alert("Proxy type must be either 'http' or 'socks'!");
119 | return false;
120 | }
121 |
122 | let resp = await Requests.post(
123 | "ajax/checkAccount.php",
124 | `name=${encodeURIComponent(name)}&token=${encodeURIComponent(token)}&cookies=${encodeURIComponent(cookies)}&proxy=${encodeURIComponent(proxy)}`
125 | );
126 | let checkResp = await Requests.checkResponse(resp);
127 | if (!checkResp.success) {
128 | alert(`Error checking account: ${checkResp.error}`);
129 | return false;
130 | }
131 |
132 | return true;
133 | }
--------------------------------------------------------------------------------
/scripts/actions.js:
--------------------------------------------------------------------------------
1 | import {Requests} from "./requests.js";
2 |
3 | export class Actions {
4 | static async sendAdAppeal(socname, accId, adId) {
5 | const resp = await Requests.post(
6 | "ajax/disapproveAppeal.php",
7 | `acc_name=${encodeURIComponent(socname)}&accid=${encodeURIComponent(accId)}&adid=${encodeURIComponent(adId)}`
8 | );
9 | let checkRes = await Requests.checkResponse(resp);
10 | if (checkRes.success)
11 | alert("Disapprove appeal sent!");
12 | else
13 | alert(`Error sending appeal: ${checkRes.error}`);
14 | }
15 |
16 | static async startAd(socname, adId) {
17 | const resp = await Requests.post(
18 | "ajax/startAd.php",
19 | `acc_name=${encodeURIComponent(socname)}&adid=${encodeURIComponent(adId)}`
20 | );
21 | let checkRes = await Requests.checkResponse(resp);
22 | if (checkRes.success)
23 | alert("Ad started!");
24 | else
25 | alert(`Error starting ad: ${checkRes.error}`);
26 | }
27 |
28 | static async stopAd(socname, adId) {
29 | const resp = await Requests.post(
30 | "ajax/stopAd.php",
31 | `acc_name=${encodeURIComponent(socname)}&adid=${encodeURIComponent(adId)}`
32 | );
33 | let checkRes = await Requests.checkResponse(resp);
34 | if (checkRes.success)
35 | alert("Ad stopped!");
36 | else
37 | alert(`Error stopping ad: ${checkRes.error}`);
38 | }
39 |
40 | static async sendAccAppeal(socname, accId) {
41 | const resp = await Requests.post(
42 | "ajax/policyAppeal.php",
43 | `acc_name=${encodeURIComponent(socname)}&accid=${encodeURIComponent(accId)}`
44 | );
45 | let checkRes = await Requests.checkResponse(resp);
46 | if (checkRes.success)
47 | alert("Appeal successfully sent!");
48 | else
49 | alert(`Error sending appeal: ${checkRes.error}`);
50 | }
51 |
52 | static async payUnsettled(socname, accId, paymentId, currency) {
53 | let sum = prompt("Enter money amount to pay:");
54 | if (!sum) return;
55 | const resp = await Requests.post(
56 | "ajax/payUnsettled.php",
57 | `acc_name=${encodeURIComponent(socname)}&accid=${encodeURIComponent(accId)}&paymentid=${paymentId}&sum=${sum}¤cy=${currency}`
58 | );
59 | let checkRes = await Requests.checkResponse(resp);
60 | if (checkRes.success)
61 | alert("Payment processed successfully!");
62 | else
63 | alert(`Error processing payment: ${checkRes.error}`);
64 | }
65 |
66 | static downloadRules(accId) {
67 | let accWithRules = window.adAcounts.filter(acc => acc.id === accId);
68 | if (accWithRules.length != 1) {
69 | alert(`Can't find Ad Account with id ${accId}!`);
70 | return;
71 | }
72 | const fileName = prompt("Please enter the name for the the Rules JSON file:");
73 |
74 | if (!fileName) {
75 | return; // Exit if the user cancels or enters an empty file name
76 | }
77 |
78 | // Convert the JSON object to a string and create a Blob from it
79 | const jsonString = JSON.stringify(accWithRules[0].rules);
80 | const blob = new Blob([jsonString], {type: "application/json"});
81 |
82 | // Create a temporary download link and trigger the download
83 | const link = document.createElement("a");
84 | link.href = URL.createObjectURL(blob);
85 | link.download = `${fileName}.json`;
86 | link.click();
87 |
88 | // Release the object URL after the download is triggered
89 | setTimeout(() => URL.revokeObjectURL(link.href), 100);
90 | }
91 |
92 | static async deleteRules(socname, accId) {
93 | let rules = window.adAcounts.filter(acc => acc.id == accId)[0].rules.map(r => r.id).join(',');
94 | const resp = await Requests.post(
95 | "ajax/delRules.php",
96 | `acc_name=${encodeURIComponent(socname)}&rules=${encodeURIComponent(rules)}`
97 | );
98 | let checkRes = await Requests.checkResponse(resp);
99 | if (checkRes.success)
100 | alert("All rules deleted!");
101 | else
102 | alert(`Error deleting rules: ${checkRes.error}`);
103 | }
104 |
105 | static async uploadRules(socname, accId) {
106 | const fileInput = document.createElement("input");
107 | fileInput.type = "file";
108 | fileInput.accept = ".json";
109 |
110 | fileInput.addEventListener("change", async (event) => {
111 | const file = event.target.files[0];
112 | if (file) {
113 | try {
114 | const fileContent = await Actions.readFileAsText(file);
115 | await Actions.sendRulesToServer(socname, accId, fileContent);
116 | } catch (error) {
117 | console.error("Error while uploading rules:", error);
118 | }
119 | }
120 | // Remove the file input element after it's been used
121 | fileInput.remove();
122 | });
123 |
124 | // Trigger the file selection dialog
125 | fileInput.click();
126 | }
127 |
128 | static readFileAsText(file) {
129 | return new Promise((resolve, reject) => {
130 | const reader = new FileReader();
131 | reader.onload = () => resolve(reader.result);
132 | reader.onerror = () => reject(reader.error);
133 | reader.readAsText(file);
134 | });
135 | }
136 |
137 | static async sendRulesToServer(socname, accId, jsonString) {
138 | const resp = await Requests.post(
139 | "ajax/uploadRules.php",
140 | `acc_name=${encodeURIComponent(socname)}&accid=${encodeURIComponent(accId)}&rules=${encodeURIComponent(jsonString)}`
141 | );
142 | let checkRes = await Requests.checkResponse(resp);
143 | if (checkRes.success)
144 | alert("Rules uploaded successfully!");
145 | else
146 | alert(`Error uploading rules: ${checkRes.error}`);
147 | }
148 | }
--------------------------------------------------------------------------------
/scripts/ad.js:
--------------------------------------------------------------------------------
1 | import {MathHelpers} from "./mathhelpers.js";
2 |
3 | export class Ad {
4 | constructor(adData, insightsData) {
5 | this.id = adData.id;
6 | this.name = adData.name;
7 | this.status = adData.effective_status;
8 | this.reviewFeedback = adData.ad_review_feedback?.global ? Object.keys(adData.ad_review_feedback.global).join('
') : '';
9 | this.impressions = adData.impressions ?? 0;
10 | this.clicks = parseInt(adData.insights.data[0].inline_link_clicks ?? 0);
11 | this.results = parseInt(insightsData[this.id] !== undefined ? insightsData[this.id].results : 0);
12 | this.spend = MathHelpers.mathMoney(adData.spent ?? 0);
13 | this.CPA = MathHelpers.mathStat(insightsData[this.id] !== undefined ? insightsData[this.id].cost_per_result: 0);
14 | this.CTR = MathHelpers.mathStat(adData.insights.data[0].inline_link_click_ctr ?? 0);
15 | this.CPM = MathHelpers.mathStat(adData.insights.data[0].cpm ?? 0);
16 | this.CPC = MathHelpers.mathStat(adData.insights.data[0].cpc ?? 0);
17 | this.adcreative = adData.adcreatives.data[0];
18 | this.imageUrl = this.adcreative.image_url ?? '';
19 | this.thumbUrl = this.adcreative.thumbnail_url ?? '';
20 | this.link = this.adcreative.object_story_spec?.link_data?.link ?? '';
21 | this.urlparams = this.adcreative.url_tags ?? '';
22 | this.qualityScore = adData.insights.data[0].quality_ranking ?? "UNKNOWN";
23 | this.engagementScore = adData.insights.data[0].engagement_rate_ranking ?? "UNKNOWN";
24 | this.conversionScore =adData.insights.data[0].conversion_rate_ranking ?? "UNKNOWN";
25 | }
26 |
27 | isActive() {
28 | return ['ACTIVE', 'CAMPAIGN_PAUSED', 'ADSET_PAUSED', 'PENDING_REVIEW'].includes(this.status);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/scripts/adaccount.js:
--------------------------------------------------------------------------------
1 | import {Ad} from "./ad.js";
2 | import {MathHelpers} from "./mathhelpers.js";
3 |
4 | export class AdAccount {
5 | constructor(accountData, insightsData, accName) {
6 | this.id = accountData.id.replace(/^act_/, '');
7 | this.socname = accName;
8 | this.name = accountData.name;
9 | this.pixelid = accountData.adspixels?.data[0]?.id ?? "";
10 | this.spendlimit = accountData.adtrust_dsl;
11 | this.billing = MathHelpers.mathMoney(accountData.adspaymentcycle?.data?.[0]?.threshold_amount ?? 0);
12 | this.curspend = MathHelpers.mathStat(accountData.current_unbilled_spend?.amount ?? 0);
13 | this.totalspend = MathHelpers.mathMoney(accountData.amount_spent ?? 0);
14 | this.paymentinfo = accountData.funding_source_details?.display_string ?? "";
15 | this.paymentid = accountData.funding_source_details?.id??0;
16 | this.currency = accountData.currency;
17 | this.timezone = accountData.timezone_name;
18 | this.status = accountData.account_status;
19 | this.disable_reason = accountData.disable_reason;
20 | this.adspaymentcycle = accountData.adspaymentcycle;
21 | this.funding_source_details = accountData.funding_source_details;
22 | this.adspixels = accountData.adspixels;
23 | this.insights = accountData.insights;
24 | this.ads = accountData.ads ? accountData.ads.data.map(adData => new Ad(adData,insightsData)) : [];
25 | this.createdtime = accountData.created_time;
26 | this.rules = accountData.adrules_library?.data ?? [];
27 |
28 | this.totalStats = {
29 | results: 0,
30 | CPA: [],
31 | spend: 0,
32 | clicks: 0,
33 | CPC: [],
34 | CTR: [],
35 | impressions: 0,
36 | CPM: []
37 | };
38 | this.ads.forEach((ad) => {
39 | this.totalStats.results += ad.results;
40 | this.totalStats.CPA.push(ad.CPA);
41 | this.totalStats.spend += ad.spend;
42 | this.totalStats.clicks += ad.clicks;
43 | this.totalStats.CPC.push(ad.CPC);
44 | this.totalStats.CTR.push(ad.CTR);
45 | this.totalStats.impressions += ad.impressions;
46 | this.totalStats.CPM.push(ad.CPM);
47 | });
48 | this.totalStats.spend = MathHelpers.mathStat(this.totalStats.spend);
49 | }
50 |
51 | isActive() {
52 | return this.status != 2;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/scripts/constants.js:
--------------------------------------------------------------------------------
1 | export const ranking = {
2 | "BELOW_AVERAGE_10": "<10",
3 | "BELOW_AVERAGE_20": "<20",
4 | "BELOW_AVERAGE_35": "<35",
5 | "AVERAGE": "50",
6 | "ABOVE_AVERAGE": ">50",
7 | "UNKNOWN": "N/A"
8 | };
9 |
10 | export const disable_reasons = [
11 | '',
12 | 'ADS_INTEGRITY_POLICY',
13 | 'ADS_IP_REVIEW',
14 | 'RISK_PAYMENT',
15 | 'GRAY_ACCOUNT_SHUT_DOWN',
16 | 'ADS_AFC_REVIEW',
17 | 'BUSINESS_INTEGRITY_RAR',
18 | 'PERMANENT_CLOSE',
19 | 'UNUSED_RESELLER_ACCOUNT'
20 | ];
21 |
22 | export const account_statuses = {
23 | 1: 'ACTIVE',
24 | 2: 'DISABLED',
25 | 3: 'UNSETTLED',
26 | 7: 'PENDING_RISK_REVIEW',
27 | 8: 'PENDING_SETTLEMENT',
28 | 9: 'IN_GRACE_PERIOD',
29 | 100: 'PENDING_CLOSURE',
30 | 101: 'CLOSED',
31 | 201: 'ANY_ACTIVE',
32 | 202: 'ANY_CLOSED'
33 | };
34 |
--------------------------------------------------------------------------------
/scripts/index.js:
--------------------------------------------------------------------------------
1 | import {loadAllStatistics} from './statistics.js';
2 |
3 | window.addEventListener('DOMContentLoaded', () => {
4 | const loadingIcon = document.getElementById('loadingIcon');
5 | const loadButton = document.getElementById("loadstats");
6 | loadButton.addEventListener('click', async (event) => {
7 | loadingIcon.style.display = 'inline-block';
8 | await loadAllStatistics();
9 | setPopups();
10 | loadingIcon.style.display = 'none';
11 | });
12 | });
13 |
14 | function setPopups() {
15 | var posters = document.querySelectorAll('.poster');
16 |
17 | posters.forEach(function (poster) {
18 | poster.addEventListener('mouseenter', function () {
19 | var descr = this.querySelector('.descr');
20 |
21 | // Check if hovered row is one of the last two rows
22 | if (Array.from(posters).slice(-2).includes(this)) {
23 | // Show .descr block above the row
24 | descr.style.top = 'auto';
25 | descr.style.bottom = '100%';
26 | descr.style.display = 'block';
27 | descr.style.position = 'absolute';
28 | descr.style.zIndex = '9999';
29 | } else {
30 | // Show .descr block below the row
31 | descr.style.display = 'block';
32 | descr.style.position = 'absolute';
33 | descr.style.zIndex = '9999';
34 | }
35 | });
36 |
37 | poster.addEventListener('mouseleave', function () {
38 | // Hide .descr block when not hovered
39 | this.querySelector('.descr').style.display = 'none';
40 | });
41 | });
42 | };
43 |
--------------------------------------------------------------------------------
/scripts/mathhelpers.js:
--------------------------------------------------------------------------------
1 | export class MathHelpers {
2 | static average(arr) {
3 | const validNumbers = arr
4 | .map(Number)
5 | .filter(num => !isNaN(num) && num!==0);
6 |
7 | if (validNumbers.length === 0) {
8 | return 0;
9 | }
10 |
11 | const sum = validNumbers.reduce((acc, num) => acc + num, 0);
12 | const avg = (sum / validNumbers.length).toFixed(2);
13 |
14 | return parseFloat(avg);
15 | }
16 |
17 | static mathMoney(num) {
18 | if (typeof num !== undefined && num !== null && num !== 0) {
19 | num = parseFloat(num).toFixed(2) / 100;
20 | } else num = 0;
21 | return num;
22 | }
23 |
24 | static mathStat(num) {
25 | if (typeof num !== undefined && num !== null && num !== 0) {
26 | num = parseFloat(num).toFixed(2);
27 | } else num = 0;
28 | return num;
29 | }
30 | }
--------------------------------------------------------------------------------
/scripts/requests.js:
--------------------------------------------------------------------------------
1 | export class Requests {
2 | static async getStats(accName, datetime) {
3 | let url = `/ajax/getAccountStatistics.php?acc_name=${accName}&datetime=${datetime}`;
4 | let resp = await fetch(url);
5 | let checkRes = await this.checkResponse(resp);
6 | if (checkRes.success) return checkRes.data;
7 | alert(`${accName}: ${checkRes.error}`);
8 | return null;
9 | }
10 |
11 | static async post(url, body) {
12 | return await fetch(url, {
13 | method: "POST",
14 | headers: {
15 | "Content-Type": "application/x-www-form-urlencoded",
16 | },
17 | body: body
18 | });
19 | }
20 |
21 | static async checkResponse(resp, getBody = true) {
22 | if (resp.status === 200) {
23 | if (!getBody) return {success: true};
24 | let t = await resp.text();
25 | let json;
26 | try {
27 | json = JSON.parse(t);
28 | } catch {
29 | return {success: false, error: `Error parsing response JSON: ${t}`};
30 | }
31 | if (json.error) {
32 | if (typeof json.error === 'object')
33 | json.error = JSON.stringify(json.error);
34 | else if (Number.isInteger(json.error) && json.errorDescription && json.errorSummary)
35 | json.error = `${json.errorSummary}: ${json.errorDescription}`;
36 | return {success: false, error: json.error};
37 | }
38 | return {success: true, data: json};
39 | } else {
40 | return {success: false, error: `Got error ${resp.status} from the server!`};
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/scripts/statistics.js:
--------------------------------------------------------------------------------
1 | import {TableFormatter} from "./tableformatter.js";
2 | import {Requests} from "./requests.js";
3 | import {AdAccount} from "./adaccount.js";
4 |
5 | export async function loadAllStatistics() {
6 | let datetime = document.getElementById('dateRange').value;
7 | const accNamesSelect = document.getElementById('accNames');
8 | const selectedAccounts = accNamesSelect.value === 'all'
9 | ? Array.from(accNamesSelect.options).filter(option => option.value !== 'all').map(option => option.value)
10 | : [accNamesSelect.value];
11 |
12 | const allAccs = await Promise.all(selectedAccounts.map(aName => load(aName, datetime)));
13 | window.adAcounts = allAccs.flat();
14 | const tf = new TableFormatter();
15 | tf.formatAdAccounts(window.adAcounts);
16 | }
17 |
18 | async function load(accName, datetime) {
19 | try {
20 | let adAccounts = [];
21 | let rawAccounts = await Requests.getStats(accName, datetime);
22 | for (const accIndex in rawAccounts.stats.data) {
23 | const rawAccount = rawAccounts.stats.data[accIndex];
24 | adAccounts.push(new AdAccount(rawAccount, rawAccounts.insights, accName));
25 | }
26 | return adAccounts;
27 | } catch (error) {
28 | console.error('An error occured getting accounts:', error);
29 | return [];
30 | }
31 | }
--------------------------------------------------------------------------------
/scripts/tableformatter.js:
--------------------------------------------------------------------------------
1 | import {account_statuses, disable_reasons, ranking} from "./constants.js";
2 | import {MathHelpers} from "./mathhelpers.js";
3 | import {Actions} from "./actions.js";
4 |
5 | export class TableFormatter {
6 | addTableHeader(parent) {
7 | let tr = document.createElement('tr');
8 | const headers = [
9 | 'Creo', 'Name/Link', 'Actions', 'Status/Ranking',
10 | 'Results', 'CPA', 'Spend', 'Clicks', 'CPC', 'CTR',
11 | 'Impres.', 'CPM'
12 | ];
13 | headers.forEach(headerText => {
14 | const th = document.createElement('th');
15 | th.textContent = headerText;
16 | tr.appendChild(th);
17 | });
18 | parent.appendChild(tr);
19 | }
20 |
21 | formatAdAccounts(allAccs) {
22 | let statBody = document.getElementById('statBody');
23 | statBody.innerHTML = '';
24 | let showAll = document.getElementById('showParam').value === 'all'; //active or all
25 | if (!showAll) allAccs = allAccs.filter(acc => acc.isActive());
26 | this.addTableHeader(statBody);
27 | allAccs.forEach(acc => {
28 | statBody.appendChild(this.createAccountRow(acc, showAll));
29 | let resAds = acc.ads;
30 | if (!showAll) resAds = resAds.filter(ad => ad.isActive());
31 | resAds.forEach(ad => statBody.appendChild(this.createAdRow(acc, ad)));
32 | });
33 | this.addActions();
34 | }
35 |
36 | getAdStatusColor(status) {
37 | switch (status) {
38 | case 'ACTIVE':
39 | return 'Lime';
40 | case 'ADSET_PAUSED':
41 | case 'CAMPAIGN_PAUSED':
42 | case 'PENDING_REVIEW':
43 | return 'Gold';
44 | default:
45 | return 'Red';
46 | }
47 | }
48 |
49 | createAccountRow(acc) {
50 | const tr = document.createElement('tr');
51 | tr.id = acc.id;
52 | tr.className = 'poster';
53 |
54 | const ascolor = acc.status === 1 || acc.status === 'ACTIVE' ? 'Lime' : 'Red';
55 | const accountStatusText = `${account_statuses[acc.status]}`;
56 | const drcolor = acc.disable_reason === 0 ? 'Lime' : 'Red';
57 | const disableReasonText = `${disable_reasons[acc.disable_reason]}`;
58 |
59 | const accountInfo = `${acc.socname}: ${acc.name} - ${acc.spendlimit}/${acc.billing}/${acc.curspend}`;
60 | const popupInfo = `
61 | ID: ${acc.id}
62 | Pixel: ${acc.pixelid}
63 | Card: ${acc.paymentinfo}
64 | Total spend: ${acc.totalspend} ${acc.currency}
65 | Time zone: ${acc.timezone}
66 | Created: ${acc.createdtime}`;
67 | let actions = this.getAccActions(acc);
68 | const rowData = `
69 |
70 | ${popupInfo} ${accountInfo}
71 | |
72 | ${actions} |
73 | ${accountStatusText} ${disableReasonText} |
74 | ${acc.totalStats.results} |
75 | ${MathHelpers.average(acc.totalStats.CPA)} |
76 | ${acc.totalStats.spend} |
77 | ${acc.totalStats.clicks} |
78 | ${MathHelpers.average(acc.totalStats.CPC)} |
79 | ${MathHelpers.average(acc.totalStats.CTR)}% |
80 | ${acc.totalStats.impressions} |
81 | ${MathHelpers.average(acc.totalStats.CPM)} | `;
82 |
83 | tr.style = 'box-shadow: rgba(15, 17, 19, 0.63) 0px 0px 20px 2px;';
84 | tr.innerHTML = rowData;
85 | return tr;
86 | }
87 |
88 | createAdRow(acc, ad) {
89 | const imageCell = this.getImageCell(ad.imageUrl, ad.thumbUrl);
90 | let link = ad.link;
91 | if (ad.urlparams) link += ad.urlparams;
92 | const name = ad.link ? `${ad.name}` : ad.name;
93 | const esColor = this.getAdStatusColor(ad.status);
94 | const rank = `${ranking[ad.engagementScore]}|${ranking[ad.conversionScore]}|${ranking[ad.qualityScore]}`;
95 | let status = ad.status;
96 | if (ad.reviewFeedback) status += `
${ad.reviewFeedback}`;
97 | if (ad.qualityScore && ad.qualityScore !== "UNKNOWN") status += `
${rank}`;
98 | const tdArray = [
99 | imageCell,
100 | `${name}`,
101 | `${this.getAdActions(acc, ad)}`,
102 | `${status}
`,
103 | `${ad.results}`,
104 | `${ad.CPA}`,
105 | `${ad.spend}`,
106 | `${ad.clicks}`,
107 | `${ad.CPC}`,
108 | `${ad.CTR}%`,
109 | `${ad.impressions}`,
110 | `${ad.CPM}`
111 | ];
112 | const tr = document.createElement('tr');
113 | for (const tdContent of tdArray) {
114 | const td = document.createElement('td');
115 | td.innerHTML = tdContent;
116 | tr.appendChild(td);
117 | }
118 | return tr;
119 | }
120 |
121 |
122 | getAccActions(acc) {
123 | let actions = "";
124 | if (acc.status == 2 && acc.disable_reason == 1) // DISABLED ADS_INTEGRITY_POLICY
125 | actions += ` `;
126 | else if ((acc.status == 3 || acc.status==1) && acc.paymentinfo) //UNSETTLED or ACTIVE + has card
127 | actions += ` `;
128 | if (acc.rules.length > 0) {
129 | actions += `${acc.rules.length} `;
130 | actions += ` `;
131 | }
132 | if (acc.status != 2)
133 | actions += ` `;
134 | return actions;
135 | }
136 |
137 | getAdActions(acc, ad) {
138 | if (acc.status == 2) return ''; //When acc DISABLED = no actions for ads
139 | switch (ad.status) {
140 | case 'DISAPPROVED':
141 | return ``;
142 | break;
143 | case 'PAUSED':
144 | return ``;
145 | break;
146 | case 'ACTIVE':
147 | return ``;
148 | break;
149 | default:
150 | return '';
151 | }
152 | }
153 |
154 | getImageCell(fullImageUrl, thumbnailUrl) {
155 | if (fullImageUrl) {
156 | return ` | `;
157 | } else if (thumbnailUrl) {
158 | return ` | `;
159 | } else {
160 | return "Thumbnail unavailable, ad deleted | ";
161 | }
162 | }
163 |
164 | addActions() {
165 | const downloadRulesButtons = document.querySelectorAll('.downrules');
166 | downloadRulesButtons.forEach(button => {
167 | button.addEventListener('click', (event) => {
168 | const accId = event.target.dataset.accid;
169 | const originalClassNames = event.target.className;
170 | event.target.className = ' fas fa-spinner fa-spin';
171 | event.target.disabled = true;
172 | Actions.downloadRules(accId);
173 | event.target.className = originalClassNames;
174 | event.target.disabled = false;
175 | });
176 | });
177 |
178 | const uploadRulesButtons = document.querySelectorAll('.uprules');
179 | uploadRulesButtons.forEach(button => {
180 | button.addEventListener('click', async (event) => {
181 | const socname = event.target.dataset.socname;
182 | const accId = event.target.dataset.accid;
183 | const originalClassNames = event.target.className;
184 | event.target.className = ' fas fa-spinner fa-spin';
185 | event.target.disabled = true;
186 | await Actions.uploadRules(socname, accId);
187 | event.target.className = originalClassNames;
188 | event.target.disabled = false;
189 | });
190 | });
191 |
192 | const delRulesButtons = document.querySelectorAll('.delrules');
193 | delRulesButtons.forEach(button => {
194 | button.addEventListener('click', async (event) => {
195 | const socname = event.target.dataset.socname;
196 | const accId = event.target.dataset.accid;
197 | const originalClassNames = event.target.className;
198 | event.target.className = ' fas fa-spinner fa-spin';
199 | event.target.disabled = true;
200 | await Actions.deleteRules(socname, accId);
201 | event.target.className = originalClassNames;
202 | event.target.disabled = false;
203 | });
204 | });
205 |
206 | const payButtons = document.querySelectorAll('.payunsettled');
207 | payButtons.forEach(button => {
208 | button.addEventListener('click', async (event) => {
209 | const socname = event.target.dataset.socname;
210 | const accId = event.target.dataset.accid;
211 | const paymentId = event.target.dataset.paymentid;
212 | const currency = event.target.dataset.currency;
213 | const originalClassNames = event.target.className;
214 | event.target.className = ' fas fa-spinner fa-spin';
215 | event.target.disabled = true;
216 | await Actions.payUnsettled(socname, accId, paymentId, currency);
217 | event.target.className = originalClassNames;
218 | event.target.disabled = false;
219 | });
220 | });
221 |
222 | const appealButton = document.querySelectorAll('.sendappeal');
223 | appealButton.forEach(button => {
224 | button.addEventListener('click', async (event) => {
225 | const socname = event.target.dataset.socname;
226 | const accId = event.target.dataset.accid;
227 |
228 | const originalClassNames = event.target.className;
229 | event.target.className = ' fas fa-spinner fa-spin';
230 | event.target.disabled = true;
231 | await Actions.sendAccAppeal(socname, accId);
232 | event.target.className = originalClassNames;
233 | event.target.disabled = false;
234 | });
235 | });
236 |
237 | const startButton = document.querySelectorAll('.startad');
238 | startButton.forEach(button => {
239 | button.addEventListener('click', async (event) => {
240 | const socname = event.target.dataset.socname;
241 | const adId = event.target.dataset.adid;
242 |
243 | const originalClassNames = event.target.className;
244 | event.target.className = ' fas fa-spinner fa-spin';
245 | event.target.disabled = true;
246 | await Actions.startAd(socname, adId);
247 | event.target.className = originalClassNames;
248 | event.target.disabled = false;
249 | });
250 | });
251 |
252 | const stopButton = document.querySelectorAll('.stopad');
253 | stopButton.forEach(button => {
254 | button.addEventListener('click', async (event) => {
255 | const socname = event.target.dataset.socname;
256 | const adId = event.target.dataset.adid;
257 | const originalClassNames = event.target.className;
258 | event.target.className = ' fas fa-spinner fa-spin';
259 | event.target.disabled = true;
260 | await Actions.stopAd(socname, adId)
261 | event.target.className = originalClassNames;
262 | event.target.disabled = false;
263 | });
264 | });
265 |
266 | const disapproveButton = document.querySelectorAll('.senddisapprove');
267 | disapproveButton.forEach(button => {
268 | button.addEventListener('click', async (event) => {
269 | const socname = event.target.dataset.socname;
270 | const adId = event.target.dataset.adid;
271 | const accId = event.target.dataset.accid;
272 |
273 | const originalClassNames = event.target.className;
274 | event.target.className = ' fas fa-spinner fa-spin';
275 | event.target.disabled = true;
276 | await Actions.sendAdAppeal(socname, accId, adId);
277 | event.target.className = originalClassNames;
278 | event.target.disabled = false;
279 | });
280 | });
281 | }
282 | }
283 |
--------------------------------------------------------------------------------
/settings.php:
--------------------------------------------------------------------------------
1 | this._items.length-1||t<0))if(this._isSliding)P(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right',trigger:"hover focus",title:"",delay:0,html:!(Ie={AUTO:"auto",TOP:"top",RIGHT:"right",BOTTOM:"bottom",LEFT:"left"}),selector:!(Se={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)"}),placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},we="out",Ne={HIDE:"hide"+Ee,HIDDEN:"hidden"+Ee,SHOW:(De="show")+Ee,SHOWN:"shown"+Ee,INSERTED:"inserted"+Ee,CLICK:"click"+Ee,FOCUSIN:"focusin"+Ee,FOCUSOUT:"focusout"+Ee,MOUSEENTER:"mouseenter"+Ee,MOUSELEAVE:"mouseleave"+Ee},Oe="fade",ke="show",Pe=".tooltip-inner",je=".arrow",He="hover",Le="focus",Re="click",xe="manual",We=function(){function i(t,e){if("undefined"==typeof h)throw new TypeError("Bootstrap tooltips require Popper.js (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=pe(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(pe(this.getTipElement()).hasClass(ke))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),pe.removeData(this.element,this.constructor.DATA_KEY),pe(this.element).off(this.constructor.EVENT_KEY),pe(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&pe(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===pe(this.element).css("display"))throw new Error("Please use show on visible elements");var t=pe.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){pe(this.element).trigger(t);var n=pe.contains(this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!n)return;var i=this.getTipElement(),r=Fn.getUID(this.constructor.NAME);i.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&pe(i).addClass(Oe);var o="function"==typeof this.config.placement?this.config.placement.call(this,i,this.element):this.config.placement,s=this._getAttachment(o);this.addAttachmentClass(s);var a=!1===this.config.container?document.body:pe(document).find(this.config.container);pe(i).data(this.constructor.DATA_KEY,this),pe.contains(this.element.ownerDocument.documentElement,this.tip)||pe(i).appendTo(a),pe(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new h(this.element,i,{placement:s,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:je},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),pe(i).addClass(ke),"ontouchstart"in document.documentElement&&pe(document.body).children().on("mouseover",null,pe.noop);var l=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,pe(e.element).trigger(e.constructor.Event.SHOWN),t===we&&e._leave(null,e)};if(pe(this.tip).hasClass(Oe)){var c=Fn.getTransitionDurationFromElement(this.tip);pe(this.tip).one(Fn.TRANSITION_END,l).emulateTransitionEnd(c)}else l()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=pe.Event(this.constructor.Event.HIDE),r=function(){e._hoverState!==De&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),pe(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(pe(this.element).trigger(i),!i.isDefaultPrevented()){if(pe(n).removeClass(ke),"ontouchstart"in document.documentElement&&pe(document.body).children().off("mouseover",null,pe.noop),this._activeTrigger[Re]=!1,this._activeTrigger[Le]=!1,this._activeTrigger[He]=!1,pe(this.tip).hasClass(Oe)){var o=Fn.getTransitionDurationFromElement(n);pe(n).one(Fn.TRANSITION_END,r).emulateTransitionEnd(o)}else r();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){pe(this.getTipElement()).addClass(Te+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||pe(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(pe(t.querySelectorAll(Pe)),this.getTitle()),pe(t).removeClass(Oe+" "+ke)},t.setElementContent=function(t,e){var n=this.config.html;"object"==typeof e&&(e.nodeType||e.jquery)?n?pe(e).parent().is(t)||t.empty().append(e):t.text(pe(e).text()):t[n?"html":"text"](e)},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getAttachment=function(t){return Ie[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)pe(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==xe){var e=t===He?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===He?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;pe(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}pe(i.element).closest(".modal").on("hide.bs.modal",function(){return i.hide()})}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||pe(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Le:He]=!0),pe(e.getTipElement()).hasClass(ke)||e._hoverState===De?e._hoverState=De:(clearTimeout(e._timeout),e._hoverState=De,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===De&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||pe(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Le:He]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=we,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===we&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){return"number"==typeof(t=l({},this.constructor.Default,pe(this.element).data(),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),Fn.typeCheckConfig(ve,t,this.constructor.DefaultType),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=pe(this.getTipElement()),e=t.attr("class").match(be);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(pe(t).removeClass(Oe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=pe(this).data(ye),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),pe(this).data(ye,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.1.3"}},{key:"Default",get:function(){return Ae}},{key:"NAME",get:function(){return ve}},{key:"DATA_KEY",get:function(){return ye}},{key:"Event",get:function(){return Ne}},{key:"EVENT_KEY",get:function(){return Ee}},{key:"DefaultType",get:function(){return Se}}]),i}(),pe.fn[ve]=We._jQueryInterface,pe.fn[ve].Constructor=We,pe.fn[ve].noConflict=function(){return pe.fn[ve]=Ce,We._jQueryInterface},We),Jn=(qe="popover",Ke="."+(Fe="bs.popover"),Me=(Ue=e).fn[qe],Qe="bs-popover",Be=new RegExp("(^|\\s)"+Qe+"\\S+","g"),Ve=l({},zn.Default,{placement:"right",trigger:"click",content:"",template:''}),Ye=l({},zn.DefaultType,{content:"(string|element|function)"}),ze="fade",Ze=".popover-header",Ge=".popover-body",$e={HIDE:"hide"+Ke,HIDDEN:"hidden"+Ke,SHOW:(Je="show")+Ke,SHOWN:"shown"+Ke,INSERTED:"inserted"+Ke,CLICK:"click"+Ke,FOCUSIN:"focusin"+Ke,FOCUSOUT:"focusout"+Ke,MOUSEENTER:"mouseenter"+Ke,MOUSELEAVE:"mouseleave"+Ke},Xe=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var r=i.prototype;return r.isWithContent=function(){return this.getTitle()||this._getContent()},r.addAttachmentClass=function(t){Ue(this.getTipElement()).addClass(Qe+"-"+t)},r.getTipElement=function(){return this.tip=this.tip||Ue(this.config.template)[0],this.tip},r.setContent=function(){var t=Ue(this.getTipElement());this.setElementContent(t.find(Ze),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(Ge),e),t.removeClass(ze+" "+Je)},r._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},r._cleanTipClass=function(){var t=Ue(this.getTipElement()),e=t.attr("class").match(Be);null!==e&&0=this._offsets[r]&&("undefined"==typeof this._offsets[r+1]||t li > .active",xn='[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',Wn=".dropdown-toggle",Un="> .dropdown-menu .active",qn=function(){function i(t){this._element=t}var t=i.prototype;return t.show=function(){var n=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&bn(this._element).hasClass(Nn)||bn(this._element).hasClass(On))){var t,i,e=bn(this._element).closest(Hn)[0],r=Fn.getSelectorFromElement(this._element);if(e){var o="UL"===e.nodeName?Rn:Ln;i=(i=bn.makeArray(bn(e).find(o)))[i.length-1]}var s=bn.Event(Dn.HIDE,{relatedTarget:this._element}),a=bn.Event(Dn.SHOW,{relatedTarget:i});if(i&&bn(i).trigger(s),bn(this._element).trigger(a),!a.isDefaultPrevented()&&!s.isDefaultPrevented()){r&&(t=document.querySelector(r)),this._activate(this._element,e);var l=function(){var t=bn.Event(Dn.HIDDEN,{relatedTarget:n._element}),e=bn.Event(Dn.SHOWN,{relatedTarget:i});bn(i).trigger(t),bn(n._element).trigger(e)};t?this._activate(t,t.parentNode,l):l()}}},t.dispose=function(){bn.removeData(this._element,Sn),this._element=null},t._activate=function(t,e,n){var i=this,r=("UL"===e.nodeName?bn(e).find(Rn):bn(e).children(Ln))[0],o=n&&r&&bn(r).hasClass(kn),s=function(){return i._transitionComplete(t,r,n)};if(r&&o){var a=Fn.getTransitionDurationFromElement(r);bn(r).one(Fn.TRANSITION_END,s).emulateTransitionEnd(a)}else s()},t._transitionComplete=function(t,e,n){if(e){bn(e).removeClass(Pn+" "+Nn);var i=bn(e.parentNode).find(Un)[0];i&&bn(i).removeClass(Nn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}if(bn(t).addClass(Nn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),Fn.reflow(t),bn(t).addClass(Pn),t.parentNode&&bn(t.parentNode).hasClass(wn)){var r=bn(t).closest(jn)[0];if(r){var o=[].slice.call(r.querySelectorAll(Wn));bn(o).addClass(Nn)}t.setAttribute("aria-expanded",!0)}n&&n()},i._jQueryInterface=function(n){return this.each(function(){var t=bn(this),e=t.data(Sn);if(e||(e=new i(this),t.data(Sn,e)),"string"==typeof n){if("undefined"==typeof e[n])throw new TypeError('No method named "'+n+'"');e[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.1.3"}}]),i}(),bn(document).on(Dn.CLICK_DATA_API,xn,function(t){t.preventDefault(),qn._jQueryInterface.call(bn(this),"show")}),bn.fn.tab=qn._jQueryInterface,bn.fn.tab.Constructor=qn,bn.fn.tab.noConflict=function(){return bn.fn.tab=An,qn._jQueryInterface},qn);!function(t){if("undefined"==typeof t)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1===e[0]&&9===e[1]&&e[2]<1||4<=e[0])throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(e),t.Util=Fn,t.Alert=Kn,t.Button=Mn,t.Carousel=Qn,t.Collapse=Bn,t.Dropdown=Vn,t.Modal=Yn,t.Popover=Jn,t.Scrollspy=Zn,t.Tab=Gn,t.Tooltip=zn,Object.defineProperty(t,"__esModule",{value:!0})});
7 | //# sourceMappingURL=bootstrap.min.js.map
--------------------------------------------------------------------------------
/styles/img/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dvygolov/ReMask/66a402344ab328da185be1898658c0b39699dd37/styles/img/background.jpg
--------------------------------------------------------------------------------
/styles/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dvygolov/ReMask/66a402344ab328da185be1898658c0b39699dd37/styles/img/favicon.png
--------------------------------------------------------------------------------
/styles/signin.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100%;
4 | }
5 | body{
6 | background: url("img/background.jpg") fixed;
7 | background-color: #2c2f36;
8 | }
9 |
10 | input{
11 | text-align:center;
12 | }
13 |
14 | select {
15 | text-align: center;
16 | text-align-last: center;
17 | /* webkit*/
18 | }
19 | select, option {
20 | padding-left: 30px;
21 | }
22 |
23 |
24 | .form-signin {
25 | width: 100%;
26 | max-width: 700px;
27 | padding: 15px;
28 | margin: auto;
29 | }
30 | .form-signin .checkbox {
31 | font-weight: 400;
32 | }
33 | .form-signin .form-control {
34 | position: relative;
35 | box-sizing: border-box;
36 | height: auto;
37 | padding: 10px;
38 | font-size: 16px;
39 | }
40 |
41 | #movable {
42 | height: 247px;
43 | overflow: hidden;
44 | width: 209px;
45 | position: absolute;
46 | -webkit-transform: rotate(-50deg);
47 | -moz-transform: rotate(-50deg);
48 | -ms-transform: rotate(-50deg);
49 | right: -500px;
50 | }
51 |
52 | .eyeball-parent {
53 | border:1px solid #000;
54 | position: absolute;
55 | top:40px;
56 | left:91px;
57 | width: 15px;
58 | height: 15px;
59 | background: #fff;
60 | border-radius: 50%;
61 | }
62 |
63 | .eyeball {
64 | position: absolute;
65 | left: 4px;
66 | top: 4px;
67 | width: 7px;
68 | height: 7px;
69 | background: #000;
70 | border-radius: 50%;
71 | }
72 |
73 | .inf{
74 | overflow: hidden;
75 | position: fixed;
76 | right: 0px;
77 | width: 118px;
78 | height: 300px;
79 | bottom: 10px;
80 | z-index:100;
81 | }
82 |
83 | .figure {
84 | display: block; /* Блочный элемент (для старых браузеров) */
85 | text-align: center; /* Выравнивание по центру */
86 | font-style: italic; /* Курсивное начертание */
87 | margin-top: 0; /* Отступ сверху */
88 | }
89 |
90 | .gotowork {
91 | -webkit-animation: preloader 1.5s infinite linear;
92 | -moz-animation: preloader 1.5s infinite linear;
93 | -ms-animation: preloader 1.5s infinite linear;
94 | -o-animation: preloader 1.5s infinite linear;
95 | animation: preloader 1.5s infinite linear;
96 | }
97 |
98 | @-webkit-keyframes preloader {
99 | to { -webkit-transform: rotate(360deg); }
100 | }
101 |
102 | @-moz-keyframes preloader {
103 | to { -moz-transform: rotate(360deg); }
104 | }
105 |
106 | @-ms-keyframes preloader {
107 | to { -ms-transform: rotate(360deg); }
108 | }
109 |
110 | @-o-keyframes preloader {
111 | to { -o-transform: rotate(360deg); }
112 | }
113 |
114 | @keyframes preloader {
115 | to { transform: rotate(360deg); }
116 | }
117 |
118 | .poster{
119 | position:relative;
120 | margin:100px auto;
121 | height: auto;
122 | width: auto;
123 | }
124 | .descr{
125 | display: none;
126 | padding: 15px;
127 | margin-top: 25px;
128 | background: #282b34;
129 | height: auto;
130 | color: #c8cccb;
131 | -moz-box-shadow: 0 5px 5px rgba(0,0,0,0.3);
132 | -webkit-box-shadow: 0 5px 5px rgba(0,0,0,0.3);
133 | box-shadow: 0 5px 5px rgba(0,0,0,0.3);
134 | }
135 |
136 | .centertable{
137 | table-layout:fixed;
138 | margin-left: auto;
139 | margin-right: auto;
140 | }
141 |
142 | .cntclmn{
143 | background-color: lightgray;
144 | color: black;
145 | }
146 | .accclmn{
147 | padding-left: 15px;
148 | }
149 |
150 | .tokenclmn{
151 | width: 450px;
152 | word-break: break-all;
153 | padding-left: 15px;
154 | }
155 | .fas {
156 | cursor: pointer;
157 | }
--------------------------------------------------------------------------------
/version.php:
--------------------------------------------------------------------------------
1 |