PageSourceSearch

https://www.hackermurphy.com/wp-content/plugins/sticky-device-registration/sticky-device.js?ver=2.5.3

js hackermurphy.com collected 2026-10-09 03:30:27 UTC 9,794 bytes, 241 lines download raw bytes

1// Capture a SureFeedback/ProjectHuddle access token from a magic-link URL
2// (?ph_access_token=…) into localStorage so the injected tool can authenticate.
3// Previously this param was ignored, so portal "share"/access links never
4// carried through and the toolbar stayed unauthenticated ("not allowed").
5(function () {
6    var t = new URLSearchParams(window.location.search).get('ph_access_token');
7    if (t) {
8        localStorage.setItem('ph_access_token', t);
9        console.log('[StickyDevice] Captured ph_access_token from URL');
10    }
11})();
12
13// Check if device already registered
14const existingDeviceId = localStorage.getItem('sticky_device_id');
15// A portal "Project Access Link" sends visitors to the client site with
16// ?ph_access_token=… and NO ?note. Gating on 'note' alone meant those links hit
17// neither branch below: no toolbar, no consent modal, no error — a dead page
18// for anyone following the link the portal itself hands out. Treat an access
19// token as an equally valid way into the consent flow.
20// (Safe: the modal markup is always rendered, and registerUrl hardcodes
21// note=1, so the REST consent check still passes.)
22const stickyParams = new URLSearchParams(window.location.search);
23const hasStickyParam = stickyParams.has('note') || stickyParams.has('ph_access_token');
24
25if (existingDeviceId) {
26    const projectId = StickyDeviceVars.projectId;
27    const apiKey = StickyDeviceVars.apiKey;
28    const stickyNoteUrl = StickyDeviceVars.stickyNoteUrl;
29    console.log('[StickyDevice] Existing device ID found:', existingDeviceId);
30    injectStickyNoteScript(
31        stickyNoteUrl,
32        projectId,
33        apiKey
34    );
35    submitDeviceToPortal(true); // Send heartbeat/update to portal
36} else if (hasStickyParam) {
37    console.log('[StickyDevice] No existing device ID found');
38    setTimeout(function () {
39        MicroModal.show('modal-1');
40    }, 100);
41}
42
43function registerDevice() {
44    // Fetch a new device ID from plugin REST endpoint.
45    // Returns the fetch promise so the caller can await before reloading —
46    // critical to prevent the "consent popup keeps appearing" bug where the
47    // page reloaded before localStorage.setItem ran.
48    console.log('[StickyDevice] registerUrl:', StickyDeviceVars.registerUrl);
49
50    const headers = {
51        'Content-Type': 'application/json',
52    };
53
54    // Add nonce for logged-in users.
55    //
56    // This used to send the nonce to EVERYONE, which broke consent on every
57    // full-page-cached site. wp_create_nonce('wp_rest') is baked into the HTML,
58    // so a cached page hands the same frozen nonce to every visitor. WordPress
59    // core's rest_cookie_check_errors() verifies ANY nonce it receives —
60    // logged in or not — and runs BEFORE our permission_callback, so a stale or
61    // wrong-user nonce turned into a hard 403 rest_cookie_invalid_nonce that the
62    // plugin could never see, let alone recover from.
63    //
64    // Anonymous visitors don't need it: the endpoint's permission_callback
65    // authorises them on the consent param alone. Sending it could only ever
66    // cost us a 403, so don't.
67    if (StickyDeviceVars.nonce && StickyDeviceVars.isLoggedIn) {
68        headers['X-WP-Nonce'] = StickyDeviceVars.nonce;
69    }
70
71    return fetch(StickyDeviceVars.registerUrl, {
72        method: 'GET',
73        credentials: 'include',
74        headers: headers
75    })
76        .then(async res => {
77            const contentType = res.headers.get('content-type') || '';
78
79            if (!res.ok) {
80                const text = await res.text();
81                console.error('[StickyDevice] Register request failed:', res.status, res.statusText);
82                console.error('[StickyDevice] Response body:', text);
83                throw new Error(`HTTP ${res.status}: ${res.statusText}`);
84            }
85
86            if (!contentType.includes('application/json')) {
87                const text = await res.text();
88                console.error('[StickyDevice] Expected JSON but got content-type:', contentType);
89                console.error('[StickyDevice] Response body:', text);
90                throw new Error('Invalid content-type: expected JSON');
91            }
92
93            return res.json();
94        })
95        .then(data => {
96            console.log('[StickyDevice] Response data:', data);
97
98            if (!data.deviceId) {
99                console.error('[StickyDevice] No device ID in response');
100                throw new Error('No device ID in response');
101            }
102
103            // Save ID in localStorage
104            localStorage.setItem('sticky_device_id', data.deviceId);
105            localStorage.setItem('sticky_device_url', data.url);
106            // console.log('[StickyDevice] Device ID saved:', data.deviceId);
107
108            // Remove consent parameter from URL
109            // const url = new URL(window.location);
110            // url.searchParams.delete('sticky_note_consent');
111            // window.history.replaceState({}, '', url);
112        })
113        .catch(err => {
114            // Log, then RE-THROW. Swallowing this is what made the bug
115            // invisible: the caller reloaded the page regardless, so a failed
116            // registration looked exactly like a successful one that simply
117            // hadn't drawn the toolbar yet. The caller needs to know.
118            console.error('[StickyDevice] Error fetching device ID:', err);
119            throw err;
120        });
121}
122
123function injectStickyNoteScript() {
124    console.log('[StickyDevice] Injecting sticky note script');
125    const url = localStorage.getItem('sticky_device_url');
126    const phAccessToken = localStorage.getItem('ph_access_token');
127    if (!url) {
128        console.error('[StickyDevice] No sticky device URL found in localStorage');
129        return;
130    }
131    const script = document.createElement('script');
132    script.type = 'text/javascript';
133    script.async = true;
134    script.defer = true;
135    script.charset = 'UTF-8';
136    script.src = url + '&v=' + (new Date()).getTime();
137    if (phAccessToken) {
138        script.src += '&ph_access_token=' + encodeURIComponent(phAccessToken);
139    }
140    const firstScript = document.getElementsByTagName('script')[0];
141    firstScript.parentNode.insertBefore(script, firstScript);
142}
143
144// Inject sticky note script if URL param indicates consent
145
146// Utility function to get current device ID
147function getStickyDeviceId() {
148    return localStorage.getItem('sticky_device_id');
149}
150
151/**
152 * const deviceRegistration = new DeviceRegistration({
153    apiEndpoint: 'https://your-wordpress-site.com/wp-json/sntd/v1/register-device',
154    apiKey: 'your-api-key-here',
155    debug: true
156});
157
158// Register device when user logs in or visits
159deviceRegistration.registerDevice('[email protected]', 123)
160    .then(result => {
161        if (result.success) {
162            console.log('Device registered:', result);
163            
164            // Start heartbeat to keep device active
165            deviceRegistration.startHeartbeat('[email protected]', 15);
166        }
167    });
168
169// Or use auto-register (checks if already registered recently)
170deviceRegistration.autoRegister('[email protected]', 123);
171 */
172
173async function submitDeviceToPortal(isUpdate = false) {
174    const { restApiKey, email: configuredEmail = '', portalUrl, projectId = 0 } = StickyDeviceVars.config;
175
176    const deviceRegistration = new DeviceRegistration({
177        apiEndpoint: `${portalUrl}/wp-json/sntd/v1/register-device`,
178        apiKey: restApiKey,
179        debug: true
180    });
181
182    const getEmail = async () => {
183        if (configuredEmail) {
184            return configuredEmail;
185        }
186
187        // An anonymous client-site visitor never has a portal session, so the
188        // cross-origin /current-user call just returns 401 after ~1s of
189        // CORS-preflight + round-trip. Skip it entirely unless logged in —
190        // this was a major contributor to the Accept-button lag.
191        if (!StickyDeviceVars.isLoggedIn) {
192            return '';
193        }
194
195        try {
196            const response = await fetch(`${portalUrl}/wp-json/sntd/v1/current-user`, {
197                credentials: 'include'
198            });
199            const data = await response.json();
200            return data?.data?.email || '';
201        } catch (error) {
202            console.error('[StickyDevice] Failed to fetch current user:', error);
203            return '';
204        }
205    };
206
207    const userEmail = await getEmail();
208
209    if (isUpdate) {
210        if (!userEmail) {
211            console.log('[StickyDevice] Cannot send heartbeat without email');
212            return;
213        }
214
215        console.log('[StickyDevice] Sending heartbeat for:', userEmail);
216        // Start heartbeat to keep device active     
217        deviceRegistration.startHeartbeat(userEmail, 15);
218        return;
219    }
220
221    try {
222        const result = await deviceRegistration.registerDevice(userEmail, projectId);
223
224        if (result.success) {
225            console.log('[StickyDevice] Device registered to portal successfully:', result);
226            if (!userEmail) {
227                console.log('[StickyDevice] Cannot send heartbeat without email');
228                return;
229            }
230            // Start heartbeat to keep device active
231            deviceRegistration.startHeartbeat(userEmail, 15);
232        } else {
233            console.error('[StickyDevice] Registration to portal failed:', result.error);
234        }
235    } catch (error) {
236        console.error('[StickyDevice] Device registration error:', error);
237    }
238}
239
240// Expose function globally for other scripts
241window.getStickyDeviceId = getStickyDeviceId;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.