@bitrix24/b24jssdk
Version:
Bitrix24 REST API JavaScript SDK
1 lines • 7.89 kB
Source Map (JSON)
{"version":3,"file":"_placement-options.mjs","sources":["../../../src/frame/_placement-options.ts"],"sourcesContent":["/**\n * What the portal actually puts in `PLACEMENT_OPTIONS`, and why the SDK has to\n * normalise it before a caller sees it.\n *\n * Traced through the `rest` module of an on-premise build (`SM_VERSION\n * 26.150.0`). The value reaches a frame through\n * `BX.rest.AppLayout.MessageInterface.getInitData()`\n * (`bitrix/js/rest/applayout.js`), which hands back `this.params.placementOptions`\n * **verbatim** — whatever the page template rendered with\n * `CUtil::PhpToJsObject()`. So the shape is decided in PHP, by four different\n * producers:\n *\n * 1. **Default placement — the frame URL's query string.**\n * `PlacementDataExtractor::run()` builds the value from\n * `request->getQueryList()->toArrayRaw()` minus `HttpRequest::getSystemParameters()`\n * and `_r`. So the keys are whatever was in the URL, in whatever case the\n * caller wrote, and the values are **strings** — or nested objects, for\n * `a[b]=c`. This is also where `IFRAME` comes from: `marketplace.js` opens the\n * frame with `IFRAME=Y` in the URL, which is why {@link isSliderMode}\n * compares against the string `'Y'` rather than a boolean.\n * 2. **Registered placement.** `app.placement/class.php` starts from an empty\n * array, merges the options the placement was bound with, and forces an empty\n * array when the parameter is not one. Values are arbitrary JSON the\n * application stored at bind time.\n * 3. **A slider opened from JS.** `BX.rest.AppLayout.openApplication()` takes the\n * caller's object, moves every `bx24_*` key out into side-panel settings and\n * `delete`s them, and — if the object has an `options` key — replaces the whole\n * thing with `placementOptions.params`. The object that arrives is not the\n * object that was passed.\n * 4. **Nothing supplied at all.** `app.layout/class.php` defaults the parameter to\n * `''`, and `CUtil::PhpToJsObject('')` renders an **empty string**.\n *\n * Case 4 is the one that broke the type. `Object.freeze` returns a primitive\n * unchanged, so `Object.freeze('')` is `''` and `Object.freeze(undefined)` is\n * `undefined` — a field declared `object` was holding neither, on a perfectly\n * ordinary portal, and the `any` on the getter meant nothing downstream noticed\n * (#485).\n *\n * A JSON **string** is normalised here too. That path does exist —\n * `app.layout/templates/.default/template.php` writes\n * `Json::encode($arParams['~PLACEMENT_OPTIONS'])` into a hidden form field — but\n * it feeds the POST re-submit, not `getInitData`, so it was not observed on the\n * handshake. Parsing it costs one `typeof` and removes a caveat the\n * documentation otherwise has to carry for ever.\n */\n\n/**\n * Placement options as a caller sees them: always an object, never `undefined`.\n *\n * Values are `unknown` rather than `string`. The default-placement path really\n * does yield strings, being a query string — but a registered placement carries\n * whatever JSON the application stored, so promising `string` would replace one\n * lie with a narrower one.\n */\nexport type PlacementOptions = Readonly<Record<string, unknown>>\n\nconst EMPTY: PlacementOptions = Object.freeze({})\n\n/**\n * A plain object — not `null`, not an array.\n *\n * Arrays are excluded because none of the four producers above was observed to\n * send one **on the build this was read from**. That is the least-evidenced arm\n * here, and the only one that discards data if it is wrong: an array, or a\n * string that parses to one, becomes `{}` rather than being carried through.\n * Producer 1 is the plausible route — PHP turns `a[0]=x&a[1]=y` into a list —\n * so if a placement is ever seen arriving as an array, this arm is the thing to\n * revisit, not the caller's code.\n *\n * The `null` arm is a **type-soundness** guard, not a behavioural one, and no\n * test can distinguish it: removing it leaves the predicate claiming `null` is a\n * `Record<string, unknown>`, while the only place the result is used spreads it\n * — and `{ ...null }` is `{}`, exactly what the guard produces anyway. A\n * mutation sweep confirmed it survives. It stays because a type predicate that\n * lies about `null` is a trap for the next use, not because anything observable\n * depends on it today.\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return 'object' === typeof value && null !== value && !Array.isArray(value)\n}\n\n/**\n * Bring whatever arrived in `PLACEMENT_OPTIONS` to a frozen object.\n *\n * Never throws and never returns `undefined`: a malformed value is indistinguishable\n * from an absent one as far as a caller can act on it, and an exception here would\n * fail the whole frame handshake over a parameter the application may not even read.\n *\n * **The copy and the freeze are one level deep.** The top-level object is this\n * module's own and is frozen; the values inside it are still the portal's\n * references, and are not frozen. So `options.payload` — once a caller has\n * narrowed it, which `unknown` forces them to — is the same object the handshake\n * data holds, and writing through it is visible there. Nothing in the SDK reads\n * `MessageInitData` again after `init()`, so this costs nothing today; it is\n * documented because the shallow copy was introduced to sever exactly that tie\n * and only severs it at the top.\n *\n * Five wire shapes collapse to `{}` and cannot be told apart afterwards: an\n * absent value, `''`, a genuinely empty object, an array, and a string that is\n * not JSON or parses to a non-object. There is deliberately no accessor for the\n * raw value — the shapes that lose information are the ones no producer above is\n * known to send. A caller who needs one of them should say so in an issue rather\n * than reach around this.\n */\nexport function normalisePlacementOptions(raw: unknown): PlacementOptions {\n try {\n if (isPlainObject(raw)) {\n // Inside the `try` as well: spreading runs the source's getters, so a\n // value carrying one that throws would take the handshake down. Nothing\n // that crossed `postMessage` can — structured clone strips accessors —\n // but `initData` is public, so \"never throws\" has to hold for what a\n // caller can hand it too, not only for what the portal sends.\n return Object.freeze({ ...raw })\n }\n\n if ('string' === typeof raw && raw.length > 0) {\n const parsed: unknown = JSON.parse(raw)\n return isPlainObject(parsed) ? Object.freeze({ ...parsed }) : EMPTY\n }\n } catch {\n // A backstop, not a handled case: none of the four producers above can\n // make a non-empty string that is not JSON. Silent rather than logged for\n // the same reason — warning about a shape nothing produces would be\n // guarding a guess, and this module has no logger to warn with. If one is\n // ever seen, that is the finding, and it belongs in an issue.\n return EMPTY\n }\n\n return EMPTY\n}\n"],"names":[],"mappings":";;;;;;;;;;AAwDA,MAAM,KAAA,GAA0B,MAAA,CAAO,MAAA,CAAO,EAAE,CAAA;AAqBhD,SAAS,cAAc,KAAA,EAAkD;AACvE,EAAA,OAAO,QAAA,KAAa,OAAO,KAAA,IAAS,IAAA,KAAS,SAAS,CAAC,KAAA,CAAM,QAAQ,KAAK,CAAA;AAC5E;AAFS,MAAA,CAAA,aAAA,EAAA,eAAA,CAAA;AA2BF,SAAS,0BAA0B,GAAA,EAAgC;AACxE,EAAA,IAAI;AACF,IAAA,IAAI,aAAA,CAAc,GAAG,CAAA,EAAG;AAMtB,MAAA,OAAO,MAAA,CAAO,MAAA,CAAO,EAAE,GAAG,KAAK,CAAA;AAAA,IACjC;AAEA,IAAA,IAAI,QAAA,KAAa,OAAO,GAAA,IAAO,GAAA,CAAI,SAAS,CAAA,EAAG;AAC7C,MAAA,MAAM,MAAA,GAAkB,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AACtC,MAAA,OAAO,aAAA,CAAc,MAAM,CAAA,GAAI,MAAA,CAAO,OAAO,EAAE,GAAG,MAAA,EAAQ,CAAA,GAAI,KAAA;AAAA,IAChE;AAAA,EACF,CAAA,CAAA,MAAQ;AAMN,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,KAAA;AACT;AAzBgB,MAAA,CAAA,yBAAA,EAAA,2BAAA,CAAA;;;;"}