├── .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 | ![Accounts Editor](accounts.jpg) 3 | ![Statistics](statistics.jpg) 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 | <?php include 'version.php' ?> 25 | 26 | 27 | 50 | 51 | 52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 62 | 65 | 66 | 67 | 68 |
name ?> 60 | [Edit] 61 | 63 | [Delete] 64 |
69 |
70 |
71 | Add Account 72 |
73 |
74 |
75 |
76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 | 93 |
94 |
95 |
96 | 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 | <?php include 'version.php' ?> 27 | 28 | 29 |
30 | 31 |
32 | Statistics 33 |
34 |
35 | 41 | 45 | 52 | 53 | 57 |
58 |
59 |
60 | 61 | 62 |
63 | 64 |
65 | 66 | 67 | 69 | 70 | -------------------------------------------------------------------------------- /menu.php: -------------------------------------------------------------------------------- 1 |

2 | Accounts | 3 | Statistics 4 |

-------------------------------------------------------------------------------- /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