Repository navigation
v1.9.78
Update now: this release includes security fixes
1.9.78 fixes security problems on tracking links, landing pages, redirectors and the account pages. Update every Prosper202 install to 1.9.78 as soon as you can. Then sign in and open My Account › User Management: if you see a user you don't recognise, remove it.
If you run Prosper202 in Docker, rebuild the image too. 1.9.78's image is built on PHP 8.3.36, which fixes two critical PHP vulnerabilities (CVE-2026-109618, CVE-2026-109619). On Coolify, redeploy; with docker compose, run docker compose build --pull, then docker compose up -d. Then check that php -v in the web container says 8.3.36.
⚠️ Back up the database before you upgrade
An upgrade changes the database in place and there is no downgrade: restoring a backup taken before the upgrade is the only way back. 1.9.78 adds a label column to the API keys table (202_api_keys): the name a key is shown with, which for a key an AI app's connection made is the app's. Coming from 1.9.76 or earlier, the upgrade also makes 1.9.77's changes: it removes the old chat's local tables, and "Delete click data older than … days" in My Account › Settings starts deleting what it says on the first cron run, so set it to 0 first if you want to keep older clicks.
Take a full mysqldump (or your host's snapshot) of the Prosper202 database after the site stops taking traffic, then replace the files with the 1.9.78 release zip and open 202-config/upgrade.php. Your install won't offer 1.9.78 in its update notice or through 1-Click Upgrade, so don't wait for a prompt. On a large install the upgrade page can outlast a proxy's time limit; the upgrade keeps running on the server, so wait a few minutes and reload the page. Step by step: Upgrading Prosper202.
Connect AI apps to Prosper202
- On your computer:
p202 mcpserves Prosper202 to AI apps that use MCP (Claude Desktop, Claude Code, Cursor and others). Each tool is marked read-only or destructive, so the app asks before a write.p202 mcp --read-onlyserves only reads, andp202 --staged mcpturns every write into a proposal for you to review. - Hosted apps: claude.ai and other hosted AI apps connect through Tracking202's MCP server,
https://mcp.tracking202.com/mcp, entered exactly as written with/mcpat the end (in ChatGPT, custom connectors need developer mode). You approve each connection on your own install, signed in as yourself, and choose read only, propose changes, or read and change. The approval makes an API key of your user with that access, labelled with the app; remove it under My Account › Personal Settings › API keys. - A hosted connection needs your install reachable from the internet over HTTPS, and reads at most about 1 MB from it for each thing the assistant asks for: on a large install, ask for less (a filter, a shorter period, fewer rows).
p202 mcpon your own computer has no such limit. - When no tool fits what you ask, the assistant can look for one (
get_more_tools): it is told which tools may do it, including ones that need a connection with more access, and Tracking202 learns what was missing. - Both need a Prosper202 ClickServer Pro subscription or trial, as the Go CLI does.
- The hosted connector records which tools an AI app calls, the names of their arguments (not their values), whether they worked, and the app's one-line description of each request, with email addresses, web addresses and long numbers removed. It keeps these with your ClickServer licence for up to two years. The approval page says so.
Download
Download prosper202-1.9.78.zip below, not GitHub's "Source code" archives. The zip bundles vendor/ and the p202 CLI binaries for Linux, macOS and Windows (amd64 and arm64), so a server needs no Composer and no Go.
Requirements: PHP 8.3+, and MySQL 8.0+ or MariaDB 10.6+, as for 1.9.77. Building the Go CLI from source now needs Go 1.25.
What's new and what changed
New
- Connect an AI assistant (Claude, ChatGPT and other apps that use MCP) to your Prosper202. It reads your reports and campaigns and, only if you allow it, proposes or makes changes. You approve each connection on your own install, and remove it under My Account › Personal Settings › API keys. Needs Prosper202 ClickServer Pro.
- p202 mcp lets AI apps on your computer (Claude Desktop, Claude Code, Cursor) work with Prosper202 through the Go CLI: read only if you choose, or with every change made a proposal you apply yourself.
Changed
- A tracking link that cannot reach the database answers 503 (try again later) where it has nowhere saved to send the visitor, instead of a broken page.
- Two Prosper202 installs that share one memcache server no longer read each other's cached entries.
- The sign-in log shows whether each attempt passed, failed or did not complete (also GET /system/login-log and p202 system login-log).
- Get Links refuses a cloaking setting other than the three it offers.
Fixed
- A new install stopped on step 2, "Get your license key", with a server error, so the key had to be entered by hand. The page now opens on a new install, before Prosper202 has created its database tables. On an install that is already set up, a license key sent back to that page is no longer repeated in the address of the sign-in page it moves you on to.
- When the database refused a change or a read failed, several pages said the change was saved, showed an empty value (a cron job that never ran, no DNI networks, no ClickServer key) or stopped on a PHP error. They now say the save failed or show the database error page, and the error is recorded.
- Direct links filled the offer URL's tokens ([[c1]] to [[c4]], [[t202kw]], [[gclid]], the UTM tokens and others) with nothing, and [[cpc]], [[payout]] and [[cpa]] wrongly. They carry the click's values again.
- A redirector rule that sends visitors to a campaign with URL rotation always sent its first URL.
- Keywords, place and ISP names, referring sites and device models longer than their columns were stored cut and stored again on every click, and an IPv6 visitor could be shown under another visitor's address.
- Keywords, c1 to c4 and gclid values holding a quote or a backslash were stored with extra backslashes.
- A redirector re-click lost the click's customer, text ad and first values.
- A sign-in the database could not record ended on a blank page, and a right password could be logged as a failed sign-in.
- The legacy reports API (v1 and v2) answered 500 for several reports and filters, and ignored a c1 to c4 filter of 0.
- The Customer API key on Personal Settings is checked with Tracking202 before it is saved, and the page says the key could not be checked, rather than not valid, when Tracking202 does not answer.
- The cron job reports its own database failures, and an hourly or daily job that fails part way is tried again at the next run instead of the next day. A brief database error no longer sends every signed-in page to the upgrade page.
- Bulk upsert (API and CLI) says which rows were stored when the database rolls a batch back, marks the rows to send again, and a retry under the same Idempotency-Key no longer stores a batch twice.
- Turning AutoCron on in Settings was refused on every install because opening a page counted as your cron job. It works now, and when the AutoCron service refuses a local or private address, Settings says to use a cron job.
- A 202-config.php from an older install, without the read-only connection, no longer stops pages, Spy and the API with a PHP error.
- p202 conversion simulate reads the click id from the field your offer URL puts [[subid]] in, and refuses instead of guessing when it cannot tell.
- On a new install running MariaDB, clicks that arrived at the same moment could go unrecorded. They are all recorded now.
Full changelog (for developers)
- Fixed: On a new install running MariaDB, clicks that arrived at the same moment could go unrecorded (the visitor was still redirected, or a landing page showed an error). They are all recorded now: the report rollup of a single click reads that click through its index instead of scanning the click table
- Fixed: Turning AutoCron on in My Account › Settings was refused with "Your own cron job ran in the last five minutes" on every install, even one with no cron job, because opening a page counted as a cron job. Pages still help run the cron's work, but no longer count as a cron job. The "Cron job last ran" tile, the cron health check, GET /system/info, GET /system/cron and p202 system cron now show when a cron job or AutoCron last ran
- Fixed: When the AutoCron service refuses an install's address (localhost, a private address or an IP address), Settings said the service "could not be reached". It now says AutoCron works only for installs at a public web address, and to use a cron job instead. Saving On while AutoCron is already on registers the install again
- Fixed: After saving a setting on the My Account pages, the message appeared at the top of the page while the page scrolled down to the setting, so it was out of sight. It now appears in the section you saved
- Fixed: A rotator link (rtr.php) that leads nowhere answers 404, where it answered an empty page with 200; it and a direct link's error page are sent with no-store, so a proxy does not keep them
- Fixed: An install whose 202-config.php predates the read-only database connection (it defines only getConnection()) no longer stops with a PHP error on the pages, Spy and the API; the read connection falls back to the main one
- Fixed: An advanced landing page's script stays valid when a visitor's device name or the page's ids hold text that is not UTF-8; such text broke the page's tracking script for that visit
- Fixed: A ClickServer key check reads the licence service's answer by its HTTP status, and an answer it does not recognise counts as no answer: the page says the key could not be checked and saves nothing
- Fixed: p202 conversion simulate takes the click id from the field the campaign's offer URL puts [[subid]] in (Awin's clickref, HitPath's c1, Partnerize's /pubref:), matching each rotated offer URL exactly. It refuses rather than guess when rotated URLs would give different ids or no campaign URL decides; --subid-field names the field then. The SubID parameters page lists the networks' published fields
- Fixed: A new install stopped on step 2, "Get your license key", with a server error, so the key had to be entered by hand. The page now opens on a new install, before Prosper202 has created its database tables. On an install that is already set up, a license key sent back to that page is no longer repeated in the address of the sign-in page it moves you on to
- New: p202 mcp serves Prosper202 to AI apps on your computer that use MCP (Claude Desktop, Claude Code, Cursor and others), as tools taken from the CLI's commands (reports, campaigns, trackers, landing pages, traffic sources, clicks, conversions and proposed changes), each marked read-only or destructive so the app asks before a write. p202 mcp --read-only serves only reads, and p202 --staged mcp turns every write into a proposal. Each call runs as its own p202 process under the profile the server started with, and an answer over 60,000 characters is refused with how to ask for less, never cut short. A proposed write whose answer is too long to show says it was proposed and names its change_id
- New: Connect claude.ai and other hosted AI apps through Tracking202's MCP server (https://mcp.tracking202.com/mcp). You approve each connection on your own install, signed in as yourself: the page shows which app asks and where the connection goes, and you choose read only, propose changes, or read and change. The approval makes a new API key of your user with that access, labelled with the app; remove it under My Account › Personal Settings › API keys (an app can also remove its own key). It needs Prosper202 ClickServer Pro, decided as for the Go CLI. GET /api/v3/connect, POST /api/v3/connect/token and POST /api/v3/connect/revoke serve it, and GET /users/{id}/api-keys answers each key's label
- Changed: Building the Go CLI from source needs Go 1.25 (it needed 1.22)
- Changed: The hosted connector at mcp.tracking202.com records which tools an AI app calls, the names of their arguments (not the values), whether they work, and the app's one-line description of what each request is for, with email addresses, web addresses and long numbers removed, kept with the install's ClickServer licence for up to two years; the approval page says so. When no tool fits, the app can call get_more_tools, which ranks the tools with p202 search, now allowed in the hosted engine
- Changed: After sign-in, Prosper202 returns you only to a page on the same install
- Changed: Discarding a proposed change needs write access to the change's area, as applying it does, or the key that proposed it; a proposal records which key staged it (as a digest, never the key). A change with no recorded area is held to the area its path names
- Fixed: The Customer API key on My Account › Personal Settings is checked with Tracking202 before it is saved, and when no answer comes it saves nothing and says the key could not be checked. The customer key and ClickServer key forms and the licence-key page also say "could not be checked", not "not valid", when no answer came, and a key is checked, saved and kept in the session as one trimmed value
- Changed: The Go CLI's licence check leaves only the API root, GET /capabilities and GET or HEAD of /system/health, /system/version and /system/info open without a licence, where every /system route was. p202 tracker bulk-urls and p202 campaign replace-url stop at the first licence refusal and end with it (exit code 6); replace-url still reports its rows and keeps its undo record. GET /capabilities asked while another request checks the same key waits for that answer instead of reporting the licence as unknown
- Fixed: Identity capture on advanced landing pages and direct links follows only a tracker of the page's own account whose campaign is that account's; any other leaves capture off
- Fixed: A tracker, offer or landing page created after a link had asked for its id was not found until the cached miss expired; a miss is no longer cached, so it is found at once. A click's request URL, referer and landing page address are looked up for each visit rather than through the cache
- Fixed: After an upgrade, the chat box went on telling Tracking202 the old version for up to a day; it sends the new one straight away
- Changed: The Docker image is built on PHP 8.3.36, the PHP release of October 21, 2026 that fixes two critical security vulnerabilities (CVE-2026-109618, CVE-2026-109619). The Dockerfile asked for PHP 8.3 without naming a patch release, so a server that had built the image before kept the PHP it had then on every rebuild. If you run Prosper202 in Docker, update and rebuild: on Coolify, redeploy; with docker compose, docker compose build --pull, then docker compose up -d. Then check that php -v in the web container says 8.3.36
- Fixed: bulk-upsert answered rows created or updated that a deadlock had rolled back. Its rows are written up to 100 at a time in one transaction; a deadlock undoes the whole of it, and the rows after it were then written outside any transaction. A batch a deadlock rolls back is now written again from the start, up to three more times, and if it still cannot be, every row of it is answered error with retryable: true, counted in summary.retryable: send those rows again under a new Idempotency-Key. A lock wait timeout is answered that way at once, and the rows after it are not attempted. A database failure after a batch was stored answered 500 and left the Idempotency-Key unspent, so the same rows sent again stored that batch twice; every row is now answered and the answer kept under the key, and a batch whose commit the database did not confirm is answered unconfirmed: true, never retryable. A row that failed after its insert (taking its public id, or being read back) was answered error and kept anyway; it is now undone, and its neighbours are stored. The change feed (GET /changes/{entity}) hears only of rows that were stored, once each
- Fixed: A failed read of the stored database version, for any reason but a missing version table, was read as version 1.0.2: every signed-in page sent the operator to the upgrade, and an upgrade run then would have started again from its first step. Only a missing table reads as 1.0.2 now; any other failure shows the database error page
- Fixed: Setup › Redirector's rule save went on past a database failure: a failed update of the redirector skipped every rule and ended in a PHP error, and a rule, redirect or criterion the database refused was dropped with the rest reported saved. Each failure now shows the database error page and is recorded. A campaign or landing page named with an apostrophe ("Mike's offer") made its redirect fail to save; it is stored as named. With Slack on, saving an existing rule sent its name and status notices with no old value
- Fixed: Get Links, removing a link and Setup › Campaigns, Categories, Text Ads and Landing Pages act on a failed database read: a link whose details could not be read was deleted anyway, a link was saved whose traffic source variables could then not be read (they are read before anything is saved now), and the Landing Page Optimizer link read a failed lookup as "not paired". After a link's edit is saved, a Slack notice whose names cannot be read is skipped and the page says so above the link
- Fixed: The pixels and postbacks (gpx.php, upx.php, gpb.php, cb202.php) record a database failure with the database errors and answer 500: gpx.php dropped the conversion with nothing recorded, and upx.php answered "Click data not found" for a failed read, or recorded a conversion and fired none of its traffic source's pixels. A direct link's CPA row the database refuses is recorded the same way, after the click's report row, which no longer waits on it
- Fixed: On a tracking link, a database session that refused its sql_mode was used anyway, and the click written under the server's own mode. It is now logged and read as an outage: the visitor goes to the URL the link keeps for one, or gets the 503 page, and nothing is written. A report rollup of a recorded click that loses a lock is tried once more, and any other failure is logged; the visitor goes on either way
- Fixed: The cron job (202-cronjobs/index.php) said nothing when its own statements failed: a failed check of whether a job had run read as "already ran", a failed record of a run let the job run again every minute, and failures of the daily cleanup were dropped. Each now ends the run with the error in cronjob.log, exit 1 on the command line and 500 over HTTP, and the hourly and daily jobs are recorded as run only once their work is done, so one that failed part way is tried again (the daily cleanup with the next hour's run) instead of waiting for the next day. A delayed statement the server refuses is logged without its values and dropped, and the due ones are cleared before they run, so none runs twice
- Fixed: The report rebuild (process_dataengine_job.php, the dirty hours and the data engine's rebuild windows) went on past a failed claim, release or mark of a window with nothing said; each failure now stops the step, logs it and names the window. health.php answered 200 with "never run" and zero counts for a read it could not make, and dni.php "Unauthorized!" and daily-email.php 200 for a database error: each answers 500, and dni.php and daily-email.php record the error
- Fixed: A sign-in with a legacy password hash, on a database where the password column could not be widened, stored the new hash cut short, so the next sign-in with the same password failed. The sign-in is refused as Not completed now, and the stored hash is kept. A failed read of the signed-in user's roles or permissions was read as none; it shows the database error page
- Fixed: The ClickServer key page showed no saved key, or "No user found", when the database could not be read; it shows the database error page, and a key it could not save is logged. The server check showed MySQL partitioning as Disabled when it could not check; it says "Could not be checked"
- Changed: The v3 API writes every 500 it answers to the server's error log, with its cause and the values MySQL's words quote taken out; it logged nothing for one
- Fixed: Settings, Personal Settings, API Integrations, ClickServers, VIP Perks and the DNI progress check act on a failed database read or write. Settings' cron and report-rebuild status, the DNI networks list and its edit form, and the ClickServer key show the database error page instead of never, none or missing; a failed save of a key, an email, the automatic deletion setting or a Landing Page Optimizer setting says it was not saved (the optimizer's pairing and context-token settings were dropped without a word); the daily email's account read records its failure. AccountPagesQueryThroughTheLoggerTest holds 202-account/ to _mysqli_query(), so MysqliQueryResultIsTestedTest reads every result there
- Fixed: On Setup › Traffic Sources, a traffic source or account the database refused to save, change or delete was reported as done, or the page stopped with a PHP error where it checked that the source was the account's. Each now shows the database error page, and the error is recorded
- Fixed: Turning AutoCron on when the cron log could not be read turned it on as though your own cron job had never run; Settings now says AutoCron was not changed. A failed read of the settings themselves showed AutoCron and MaxMind as off; it shows the database error page
- Fixed: My Account › User Management said a user it could not read had been removed already, when removing or editing them, and refused a role change it could not check as "not authorized". Each now says the user or role could not be read just now
- Fixed: Lost password: a failed account lookup said an email was on its way and sent none; the page now says the request could not be processed (as it would for any username, so it tells nobody whether an account matches). A reset key the database refuses to store is unchanged: it is logged and the page answers as it does for every request, since only a matching account gets that far. Password reset: a failed key lookup answered "No key was found like that" to a good key; it shows the database error page
- Fixed: When the signed-in user could not be read, Setup › Campaigns, Categories, Landing Pages and Text Ads, Get Links' save, removing a link, the landing page code, the redirector's rule calls and the premium upgrade page ended in a PHP error with nothing recorded, and Settings, Users and the account page drew with a blank username (the account page's preferences form with every field blank, which a save would have stored). Each now shows the database error page, and the failure is recorded
- Fixed: Removing a redirector on Setup › Redirector removes only the account's own redirector, with its rules, criteria and their redirects (the redirects were left behind), and refuses any other as not found; a removal the database refuses part way removes nothing and shows the database error page. Renaming a redirector that is not the account's, or was removed, is refused as not found, where it said it had been renamed. A redirector's save the database refused also reported success
- Fixed: Get Links reads a link's id exactly when it opens, saves or replaces a link, and refuses an edit of a link that was removed or is not the account's, where it made a new link and said the edited one was updated. The WordPress plugin's page calls (wp_create_lp, wp_update_lp) read their ids the same way: they answer error 1 for anything that is not an id, and wp_update_lp for a page that is not the account's, which it answered as saved
- Fixed: With the database unreachable, a tracking link (dl.php), a simple landing page's link (lp.php) and an offer link (off.php) answered 500 instead of sending the visitor to the URL they keep for an outage: the connection's failure ended the request before any of them could fall back. They fall back again, and a link with nothing kept answers the "temporarily unavailable" page with a 503 (lp.php and off.php answered an error sentence as a success). Every other tracking link, landing page script and pixel (rtr.php too, which keeps no URL for an outage) answers the same 503, where each failed in its own way, and the legacy report and category APIs answer their "Database connection is unavailable" JSON with a 503
- Changed: A click that a direct link (dl.php) or a landing page's script (record_simple.php, record_adv.php) cannot record is listed with the database errors, as a rotator link's always has, and no row of it is kept; any cache entry made while recording it is written only once the click is stored. The landing page's scripts show the database error page, which now answers with a 500 on every page that shows it: it answered 200, which a client or a monitor reads as served. A direct link writes its click after it has queued the visitor's redirect, so there the click is lost and the visitor still goes to the offer, on every server
- Changed: A database failure nothing on a signed-in page caught shows the database error page and is recorded, with a 500, as on the tracking links; it ended in a PHP error with nothing recorded. The cached reads the pages use now refuse a failed read rather than answer it as no row. What an uncaught failure writes to the server's error log, on the pages and the tracking links, leaves out the values the database's message quotes and every function's arguments
- Fixed: A rotator link (rtr.php) wrote a click's seven rows one statement at a time, so a database failure part way left a click with some of its rows and not others, which reports counted. The rows are now written with prepared statements in one transaction and land together or not at all; a failure still ends with the database error page, and now leaves no partial click. Where the click goes and what it records are unchanged
- Fixed: The database error page wrote its record of the error into whatever transaction the failed page had open, so when the request ended the record was rolled back with it: the page said the error "has been recorded" and the error list (GET /system/errors, p202 system errors) did not have it. The open transaction is now rolled back first and the record kept
- Fixed: When the database connection was lost, the error page itself failed: on tracking links it called itself again for each lookup it made, until PHP ran out of memory, writing tens of thousands of lines to the server's error log in one request, and on the signed-in pages it stopped with a PHP error. The error page is now shown in both cases, saying the error could not be recorded, and the server log says why; on the signed-in pages it draws its footer without the footer's two database reads (the account's email for the support widget and the cron ping's tracking domain)
- Fixed: "Remember me" could issue a sign-in cookie whose key was never stored when the database refused the write, so the browser was signed out at its next visit; and one failed read of the account's remember-me secret replaced the stored secret with a new one, signing every remembered browser of the account out. A key that is not stored is no longer handed out (you stay signed in for the session), and a failed read leaves the stored secret alone
- Fixed: The database error list (GET /system/errors, p202 system errors) showed no error text for database failures on Get Links (replacing, creating or numbering a tracker, choosing its public id, saving it) and on the redirector breakdown: the error was cleared before it was read, or filed as the statement. It is recorded as the error now
- Fixed: A failed read of the account's email for the support widget ended every signed-in page in its footer with a PHP error. The footer now leaves the address out, the page finishes, and the failed read is in the server log
- Changed: A database statement that fails on the signed-in pages is written to the server's error log, with the file and line that ran it, MySQL's error and the statement, each with the values they quote removed (MySQL's message repeats a value for a duplicate entry, for instance), however the server's sql_mode reads the quotes. These failures used to leave no trace anywhere. A failure that ends on the database error page is logged by that page instead
- Fixed: Direct links (dl.php) sent every click value an offer URL asks for empty: [[c1]] to [[c4]], [[t202kw]], [[gclid]], the UTM tokens, [[country]], [[region]], [[city]] and the others, because the URL was filled by reading the click back before it had been written. They are now filled from the click's values before the redirect; the visitor's location is looked up first only when the URL uses a location token, and a keyword or value longer than a click keeps is sent as it is kept. The outbound link of a simple landing page had the same fault and has the same fix
- Fixed: [[cpc]] was 0 and [[payout]] empty on every tracking link, [[cpa]] was 0 for a click with no CPA, and a value read back from a click was escaped for the database before it went into the URL, so a quote arrived with a backslash before it. [[cpc]] is now the click's cost to the cent, [[cpa]] its CPA (empty when it has none) and [[payout]] its payout; a URL filled for a click that is not there leaves them empty, and a failed read of the click shows the database error page and is recorded, as a failed query there already did, rather than sending the visitor on with empty values
- Fixed: A rotator rule that sends visitors to a campaign with URL rotation always sent its first URL, and logged a PHP warning for every such click. Each click now gets the campaign's next URL, as the rotator's default already did
- Fixed: The support widget's script in the page footer took the account's email as it was stored, so an address with an apostrophe broke the script, and on the page that first fetched the address from the account server it carried a backslash before the apostrophe. The address is now written as a JavaScript string, as it was fetched
- Fixed: Checking a "remember me" cookie no longer creates a remember-me key for the account; only signing in with "remember me" does
- Changed: The database error page logs the failed statement and MySQL's message with the values they quote removed, as the signed-in pages' log line does, and the error list (GET /system/errors, p202 system errors) stores them the same way; a duplicate entry keeps the name of its key. Errors recorded before this version keep the statement as it was sent
- Fixed: A keyword longer than 150 characters was cut by the database as it was stored, so the next click with it found no match and stored it again, once per click; with P202_SQL_STRICT set the click failed. Keywords are now cut to 150 characters before they are looked up and stored. A c1 to c4, UTM or variable value is cut to 350 characters, its column's width, on every path: direct links and landing pages cut it at 350 bytes, so a value of more than 87 emoji (116 characters of most Chinese or Japanese text) lost its end and two such values sharing their first 350 bytes were counted as one value. Clicks recorded before keep the value as it was cut; a later click with the same long value is filed under the whole value
- Fixed: A redirector rule or default whose campaign or landing page belongs to another account (possible for one saved by an earlier version) now treats that destination as gone, as it does a deleted one: a rule left with none passes the visitor on to the next rule or the rotator's default, where a deleted one sent them nowhere
- Fixed: An offer link's [[tokens]] (keyword, c1 to c4 and, now that they are filled, [[cpc]], [[cpa]] and [[payout]]) are filled only from a click of the link's own account
- Changed: composer.json now declares the PHP mbstring extension. Installs already needed it (install.sh checks for it); composer install now stops without it
- Fixed: A redirector link (rtr.php) stored a keyword or a c1 to c4 value holding a quote or a backslash with backslashes added (c'1\ was kept as c'1\), so reports listed it that way, an offer URL's [[c1]] to [[c4]] and [[t202kw]] sent it that way, and the same value from a direct link was counted apart. Values are now stored as they were sent, as direct links store them; clicks recorded before keep what was stored. The Landing Page Optimizer's context token carried a keyword or c1 to c4 value with a line break in it as the letter n, and on a re-click (?lpr=) a quote or backslash in the keyword escaped; it carries the value as sent
- Changed: A redirector click with no keyword now records no keyword, and one with no referring page no referring URL, as direct links do, where each stored an empty one. Like a direct link's, such a click is now left out of a keyword or referer breakdown (report breakdown) instead of counting under an empty name, and the group report lists it under [no keyword] or [no referer]
- Fixed: A tracking link lost its click to a PHP error when a value it reads from the request came as a list: the referring page's q (q[]=…) on a redirector link, direct link or landing page set to read the searched keyword, and on a direct link also a t202var_ keyword's parameter, the referring page's url and t202ref, after the visitor had been sent on. The landing pages stored such a t202var_ parameter as the keyword Array and such a url or t202ref as the referring URL Array. Such a value is now passed over, as an empty one is on all four (a direct link took an empty q or url as the value, and a landing page passed over a q of 0)
- Fixed: On a redirector link, a visitor from some IPv6 addresses got the database error page and no click was recorded, and others had their address stored garbled, with a new address row on every click; the database error page's record of such a visitor failed the same way. An IPv6 address is now stored once and found on every later click (two first clicks at the same moment can each store it, as with a new page address)
- Changed: A failure nothing caught on a tracking link, landing page or pixel answers 500. A database failure there shows the database error page and is recorded, as a failed query already was, under the line of the tracking script that reached the database (dl.php:375 for the keyword lookup); on a direct link or a landing page's click it ended in a PHP error with nothing recorded. Anything else is written to the server's error log, and nothing about it is shown
- Fixed: Looking up an IPv6 visitor's address row could return an IPv4 visitor's instead (any IPv4 address of the form N.0.x.y matched IPv6 address number N), and read the whole address index to do it, so the filter for an account owner's own clicks and a pixel's fallback to this visitor's last click could match the wrong visitor. The lookup now compares the stored address as text, through the index
- Fixed: A country, region, city or ISP name longer than its column (50 characters) or a referring host longer than 100 was stored cut and never found again, so every click carrying it added another row (with memcache, one per cache lifetime). Such values are now cut to their column before they are looked up
- Fixed: On a redirector link with memcache, a city or region id was cached under its name run together with its country's id, so Springfield1 in one country could be handed the row of Springfield in another, and a country was cached under its name while it is looked up by its code. Redirector clicks now use the same lookups as direct links, which key each value apart
- Changed: A landing page's outbound link (lp.php) no longer adds its offer URL and, when cloaked, its cloaking URL to the site URL list on every click: the click never referred to either
- Fixed: A device model name longer than its column (50 characters; an Android browser's agent can name any model) was stored cut and never found again, so every click from that agent added another device model row (with memcache, one per cache lifetime), and with P202_SQL_STRICT set the click ended on the database error page. Device, browser and platform names are now cut to 50 characters before they are looked up and stored
- Fixed: A phone and a tablet whose browsers name the same device model were both counted under the type of whichever clicked first. Chrome on Android names every model "K", so once an Android tablet had clicked, every Android phone after it was reported as a Tablet, or every tablet as Mobile when a phone came first. A device model is now kept once per type, so each click counts under the type its own browser gives, and the device breakdown lists such a model once for each type. Clicks recorded before keep the type they were given; an agent's entry memcache already holds is not used
- Fixed: When the database refused one of a new custom variable set's rows on a tracking link, the set was kept without them, and every later click with the same variables was filed under that set, which the custom variable report could not see. A set and its rows are now written together or not at all, as are the two rows of a new IPv6 address, whose first row alone was found by no lookup. A set already left without its rows stays so
- Fixed: Analyze › IPs and the IP level of the group report could show an IPv4 visitor under another visitor's IPv6 address (an IPv4 address of the form N.0.x.y as IPv6 address number N), depending on how the database server compared a number with text. They now read an address as the Visitors list and the API do
- Fixed: A sign-in the database could not record (the sign-in address, which the own-clicks filter compares clicks with) ended on a blank error page when the address lookup failed, and was let in with the address unrecorded when the update failed. Both now refuse the sign-in with "Your sign-in could not be completed. Please try again in a moment.", the login history lists the attempt as Not completed, and the login log says what failed
- Fixed: A sign-in with the right password that a database failure then stopped (storing the re-hashed password of an account with a legacy password hash) counted toward the sign-in throttle as a wrong password, so a database hiccup could lock the user out, as ten wrong passwords in 15 minutes do. Such an attempt is now logged as Not completed, which the throttle does not count, and the person is told the sign-in could not be completed. A failure while starting the session (reading whose license key the session is held to) was logged as passed and ended in a PHP error; it is now refused and logged the same way. Wrong passwords count as before
- Changed: GET /system/login-log (p202 system login-log) answers each attempt's status: passed, failed or not_completed, and ?status= (--status) lists one of them; login_success is true for passed alone, so an attempt not completed reads as not passed. My Account › Settings shows such an attempt as Not completed
- Fixed: A sign-in whose survey answer could not be read (the survey service unreachable, answering an error, or answering without the flags) reset the account's two survey flags, which decide the dashboard's survey modal and its VIP perks, as though the service had answered "no modal, no perks". They are kept as stored until the service answers
- Fixed: Two Prosper202 installs on one host, sharing a memcache server, read each other's cached entries, because the cache was named by the host and server address alone: a landing-page click on one could be filed under the other's tracker for the same public id, and any click could take the other's id for a keyword, c1-c4 value, address or location. The install (its directory and its database) is now part of every cache key's name, so each install reads only its own; every entry is looked up again once after the upgrade
- Fixed: On installs upgraded from 1.4 or earlier, which keep c1-c4 unique, two first clicks at the same moment with the same new c1-c4 value made the later one fail: that visitor got an error and no redirect. The later click now finds the earlier one's value and is recorded
- Fixed: With dynamic bids on (the account's t202b setting), a landing page click (record_simple.php, record_adv.php) or a redirector click (rtr.php) carrying a bid ended in a PHP error and was not recorded. The bid is now the click's cost, as on a direct link. A bid that is not a plain decimal ($ allowed in front), or is more than 99.99 (the most a click's spy row holds), leaves the click at its tracker's CPC, as a non-numeric bid always did, and a number refused that way is written to the server's error log; an unfilled token such as {bid} is not
- Fixed: A redirector re-click (?lpr=) rewrote the click it found by deleting its rows and writing new ones, so the click lost the customer a conversion had linked to it, its reviewed mark, its public id (every cl.php and pci.php link already handed out for it stopped working) and the URL of the landing page it was made on. A re-click now rewrites only what the redirector sets (c1 to c4, the referer, address, location and device, whether the click is cloaked, its route and its redirect) and keeps the rest, its keyword and text ad included; a cloaked re-click reuses the click's public id. A ?lpr= visit that finds no earlier click is recorded as a new click and no longer marks the attribution report for recomputation
- Fixed: A c1 to c4, UTM, gclid or tracking variable sent as a list (c1[]=x) was stored as Array, and any query parameter sent as a list was passed on to the offer as x=Array. Such a value is now stored as empty, as a missing one is, and is not passed on. A value that is not valid UTF-8 has each invalid byte stored as ?, as landing pages already did for the keyword, where a strict sql_mode refused it and lost the click
- Fixed: A gclid holding a quote or a backslash was stored with backslashes added on direct links and landing pages. It is stored as sent, cut to the 150 characters its column holds
- Fixed: With P202_SQL_STRICT set, every direct-link click without cloaking was refused by the database (an empty public id) and not recorded. Such a click is stored with no public id, as without the setting
- Changed: The four click paths read the keyword one way. A source that is empty or a list is passed over, where a direct link took an empty OVKEY= over t202kw and landing pages passed over a keyword of 0; an account with no keyword preference reads the searched keyword, its default, where landing pages and direct links recorded none; and a t202var_ keyword whose parameter is missing is no keyword, where the placeholder itself was stored
- Fixed: A redirector that sends visitors to its default landing page recorded the click under no landing page, while a rule that sends them to a landing page recorded it under that page. Both record the page now. A cloaked redirector click now stores its cl.php link, so its pci.php link leads somewhere
- Changed: A tracking link no longer writes a memcache entry for each click's own URLs (the offer URL with its subid, its cl.php and pci.php links, and the off.php?acip=…&pci=… address a landing page's PHP redirect sends it to): no other click ever read one. cl.php, pci.php, off.php and offrtr.php read a click's row uncached, so after a redirector re-click they act on the click as it now is: with memcache, a cloaked re-click that keeps the click's public id would otherwise send the visitor to the offer of the click's previous visit for three minutes
- Changed: Get Links refuses a cloaking value other than the three it offers (the campaign's setting, off, on), as the API does. A link stored earlier with another value now follows its campaign's cloaking; it was read as off, so the visitor was sent to the offer of a campaign set to cloak. Choosing "off" no longer warns that the link is not attached to any cloaking preference, and a link posted with no cloaking value (every redirector link) is stored with the campaign's setting, the form's and the API's default, where it was stored as off
- Changed: A redirector link (rtr.php) naming a tracker that has no redirector now ends without recording a click, as one naming no tracker does; it recorded a click and sent the visitor to an empty address
- Fixed: A landing page's offer link (lp.php) marks as gone to the offer only a click of the landing page's own account; with any other, the visitor goes on to the offer as with no cookie
- Changed: A click leaving its landing page for an offer (off.php), through an offer rotator (offrtr.php), by a landing page's offer link (lp.php) or by a cloaked click's link (pci.php) has what it records (its campaign and payout, its rotator route, that it went out and whether it was cloaked, its outbound and redirect URLs) written together or not at all, where a database failure part way through left some of it written. The write itself checks that the click is the account's whose offer, rotator or landing page it leaves by (a cloaked click's link names nothing but its click). Where the visitor goes is unchanged
- Fixed: A tracking link whose tracker, landing page or rotator names a record of another account (possible for one saved by an earlier version) now treats that record as not there: a direct link answers its "no destination URL configured" page, a redirector link and a landing page take no click, and another account's text ad or traffic source account is recorded as none
- Fixed: A landing page's visit takes a tracker's text ad, traffic source account and cost, and on an advanced landing page its campaign and payout, only from a tracker of the page's own account; any other reads as none, as a tracker that is not there does. The landing page's script (landing.php) and get_custom_vars.php list only the variables of the account's own traffic sources
- Fixed: A cloaked click's page (cl.php) used the first account's "cloak referer" setting for every account's clicks; it now uses the click's own account's (origin, the setting's default, for an account that never saved its preferences). The page's title names the click's campaign only when the campaign is the click's account's
- Fixed: An offer link reached without a click (off.php with no pci) sent visitors nowhere unless the offer was the first account's; it now sends them to any account's offer, and the visitor's last click is looked up among the offer's own account's clicks, where it was the first account's. A campaign with URL rotation reached that way, or through a simple landing page's cloaked link (lpc.php), turned one rotation shared by every such campaign; it now turns its own
- Fixed: An offer link (off.php) whose visitor's last click has no public id (a direct link's click without cloaking), and a cloaked click's link with a public id of 0 (pci.php, cl.php), now name no click and send the visitor on to the offer, where they acted on another click stored without a public id
- Fixed: After a redirector re-click (?lpr=), the offer URL it sent filled [[cpc]], [[payout]] and [[sourceid]] with the click's first visit's values (the first visit's cost, for one), and the click's pci.php link and a go.php link naming the click set the visitor's cookie for its first visit's campaign; off.php and offrtr.php could also read the first visit. Each now reads the click's latest visit, as cl.php and GET /clicks/{id} do
- Changed: A tracking link reads the public ids it is given (a landing page's lpip, a tracker's t202id, a campaign's acip, a rotator's rpi, a click's pci, and a click id from a cookie) only as plain whole numbers; any other value names nothing, so an offer URL's [[subid]] is no longer filled with a cookie value that is not a click id
- Changed: get_custom_vars.php and the landing page script (landing.php) answer 500 when the traffic source's variables cannot be read, where they answered as for a source with no variables
- Changed: A report on the legacy GET /api/v2/reports/ whose statement the database refuses answers {"msg": "Database error", "error": true, "status": 500}, as /api/v1/reports/ does, and both now answer it with HTTP status 500 (they answered 200), and the failure is written to the server's error log; it printed the HTML database error page in place of the JSON. The two versions' report code is now one copy (api/legacy/functions.php), so what v1 and v2 answer cannot drift apart again; v1 still answers the WordPress plugin's report types, which v2 never has
- Fixed: On the legacy GET /api/v1/reports/ and /api/v2/reports/, a key whose row, or whose account's time zone, the database could not read was refused as a deactivated account's, and on v2 answered every report in UTC; each answers the API's "Database error" under HTTP 500. The reports read the account's time zone as the rest of the app does: a zone PHP does not list is UTC, where it left the server's own zone in force
- Fixed: The WordPress plugin's calls on the legacy GET /api/v1/reports/ (get_data_for_wp, wp_create_lp, wp_update_lp) answer a statement the database refuses with the API's "Database error" under HTTP 500: a landing page update the database refused was answered as saved, and a failed read ended in a PHP error. GET /api/v2/categories/ answers the same error where it answered an empty page
- Fixed: On the legacy GET /api/v1/reports/ and /api/v2/reports/, a countries, cities or carriers report with more than one row answered 500, and one with no rows named its list locations_country, locations_city or locations_isp; each now answers every row, under the name of the report asked for, as a one-row report did
- Fixed: On the legacy GET /api/v1/reports/ and /api/v2/reports/, a report sent date_from without date_to (or date_to without date_from) answered 500; it answers "Wrong date format", as a malformed date does
- Fixed: On the legacy GET /api/v1/reports/ and /api/v2/reports/, a c1 to c4 filter of 0 was ignored and the report answered every click, and a cid of -1 answered every campaign while one such as 12abc answered campaign 12. A c1 to c4 of 0 now finds the clicks with that value, and one sent as a list (c1[]=x), which was ignored the same way, answers "c1 takes one value"; a cid that is absent, empty or 0 is every campaign, and one that is not exactly one of the account's campaign ids (012 is not) answers "Campaign not found"
- Fixed: The legacy GET /api/v2/reports/ answered 500 for every report filtered by campaign (cid), and GET /api/v2/categories/?action=list answered 500 for every account. The campaign filter answers as it does on /api/v1/reports/ (a report of that campaign's clicks, or "Campaign not found"), and the categories list lists the account's categories
- Changed: GET /clicks/{id} (p202 click get) on a click a redirector link re-clicked answers its latest visit, the one its route, redirect URL and cloaked link are; it answered whichever of the click's rows the database read first, usually the first visit. GET /clicks lists each visit, as before