From eed59972a8aec5cf4a4812c307c2cf55ffbdc05d Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 8 Oct 2020 13:26:15 +0200 Subject: [PATCH] MOBILE-3320 lint: Fix linting in text service --- src/app/services/utils/text.ts | 372 ++++++--------------------- src/app/singletons/locutus.ts | 447 +++++++++++++++++++++++++++++++++ 2 files changed, 521 insertions(+), 298 deletions(-) create mode 100644 src/app/singletons/locutus.ts diff --git a/src/app/services/utils/text.ts b/src/app/services/utils/text.ts index e6b6745ce..638c6236f 100644 --- a/src/app/services/utils/text.ts +++ b/src/app/services/utils/text.ts @@ -18,6 +18,8 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { CoreApp } from '@services/app'; import { CoreLang } from '@services/lang'; import { makeSingleton, Translate } from '@singletons/core.singletons'; +import { CoreWSExternalFile } from '@services/ws'; +import { Locutus } from '@singletons/locutus'; /** * Different type of errors the app can treat. @@ -36,53 +38,53 @@ export type CoreTextErrorObject = { export class CoreTextUtilsProvider { // List of regular expressions to convert the old nomenclature to new nomenclature for disabled features. - protected DISABLED_FEATURES_COMPAT_REGEXPS = [ - {old: /\$mmLoginEmailSignup/g, new: 'CoreLoginEmailSignup'}, - {old: /\$mmSideMenuDelegate/g, new: 'CoreMainMenuDelegate'}, - {old: /\$mmCoursesDelegate/g, new: 'CoreCourseOptionsDelegate'}, - {old: /\$mmUserDelegate/g, new: 'CoreUserDelegate'}, - {old: /\$mmCourseDelegate/g, new: 'CoreCourseModuleDelegate'}, - {old: /_mmCourses/g, new: '_CoreCourses'}, - {old: /_mmaFrontpage/g, new: '_CoreSiteHome'}, - {old: /_mmaGrades/g, new: '_CoreGrades'}, - {old: /_mmaCompetency/g, new: '_AddonCompetency'}, - {old: /_mmaNotifications/g, new: '_AddonNotifications'}, - {old: /_mmaMessages/g, new: '_AddonMessages'}, - {old: /_mmaCalendar/g, new: '_AddonCalendar'}, - {old: /_mmaFiles/g, new: '_AddonFiles'}, - {old: /_mmaParticipants/g, new: '_CoreUserParticipants'}, - {old: /_mmaCourseCompletion/g, new: '_AddonCourseCompletion'}, - {old: /_mmaNotes/g, new: '_AddonNotes'}, - {old: /_mmaBadges/g, new: '_AddonBadges'}, - {old: /files_privatefiles/g, new: 'AddonFilesPrivateFiles'}, - {old: /files_sitefiles/g, new: 'AddonFilesSiteFiles'}, - {old: /files_upload/g, new: 'AddonFilesUpload'}, - {old: /_mmaModAssign/g, new: '_AddonModAssign'}, - {old: /_mmaModBook/g, new: '_AddonModBook'}, - {old: /_mmaModChat/g, new: '_AddonModChat'}, - {old: /_mmaModChoice/g, new: '_AddonModChoice'}, - {old: /_mmaModData/g, new: '_AddonModData'}, - {old: /_mmaModFeedback/g, new: '_AddonModFeedback'}, - {old: /_mmaModFolder/g, new: '_AddonModFolder'}, - {old: /_mmaModForum/g, new: '_AddonModForum'}, - {old: /_mmaModGlossary/g, new: '_AddonModGlossary'}, - {old: /_mmaModH5pactivity/g, new: '_AddonModH5PActivity'}, - {old: /_mmaModImscp/g, new: '_AddonModImscp'}, - {old: /_mmaModLabel/g, new: '_AddonModLabel'}, - {old: /_mmaModLesson/g, new: '_AddonModLesson'}, - {old: /_mmaModLti/g, new: '_AddonModLti'}, - {old: /_mmaModPage/g, new: '_AddonModPage'}, - {old: /_mmaModQuiz/g, new: '_AddonModQuiz'}, - {old: /_mmaModResource/g, new: '_AddonModResource'}, - {old: /_mmaModScorm/g, new: '_AddonModScorm'}, - {old: /_mmaModSurvey/g, new: '_AddonModSurvey'}, - {old: /_mmaModUrl/g, new: '_AddonModUrl'}, - {old: /_mmaModWiki/g, new: '_AddonModWiki'}, - {old: /_mmaModWorkshop/g, new: '_AddonModWorkshop'}, - {old: /remoteAddOn_/g, new: 'sitePlugin_'}, + protected readonly DISABLED_FEATURES_COMPAT_REGEXPS: { old: RegExp; new: string }[] = [ + { old: /\$mmLoginEmailSignup/g, new: 'CoreLoginEmailSignup' }, + { old: /\$mmSideMenuDelegate/g, new: 'CoreMainMenuDelegate' }, + { old: /\$mmCoursesDelegate/g, new: 'CoreCourseOptionsDelegate' }, + { old: /\$mmUserDelegate/g, new: 'CoreUserDelegate' }, + { old: /\$mmCourseDelegate/g, new: 'CoreCourseModuleDelegate' }, + { old: /_mmCourses/g, new: '_CoreCourses' }, + { old: /_mmaFrontpage/g, new: '_CoreSiteHome' }, + { old: /_mmaGrades/g, new: '_CoreGrades' }, + { old: /_mmaCompetency/g, new: '_AddonCompetency' }, + { old: /_mmaNotifications/g, new: '_AddonNotifications' }, + { old: /_mmaMessages/g, new: '_AddonMessages' }, + { old: /_mmaCalendar/g, new: '_AddonCalendar' }, + { old: /_mmaFiles/g, new: '_AddonFiles' }, + { old: /_mmaParticipants/g, new: '_CoreUserParticipants' }, + { old: /_mmaCourseCompletion/g, new: '_AddonCourseCompletion' }, + { old: /_mmaNotes/g, new: '_AddonNotes' }, + { old: /_mmaBadges/g, new: '_AddonBadges' }, + { old: /files_privatefiles/g, new: 'AddonFilesPrivateFiles' }, + { old: /files_sitefiles/g, new: 'AddonFilesSiteFiles' }, + { old: /files_upload/g, new: 'AddonFilesUpload' }, + { old: /_mmaModAssign/g, new: '_AddonModAssign' }, + { old: /_mmaModBook/g, new: '_AddonModBook' }, + { old: /_mmaModChat/g, new: '_AddonModChat' }, + { old: /_mmaModChoice/g, new: '_AddonModChoice' }, + { old: /_mmaModData/g, new: '_AddonModData' }, + { old: /_mmaModFeedback/g, new: '_AddonModFeedback' }, + { old: /_mmaModFolder/g, new: '_AddonModFolder' }, + { old: /_mmaModForum/g, new: '_AddonModForum' }, + { old: /_mmaModGlossary/g, new: '_AddonModGlossary' }, + { old: /_mmaModH5pactivity/g, new: '_AddonModH5PActivity' }, + { old: /_mmaModImscp/g, new: '_AddonModImscp' }, + { old: /_mmaModLabel/g, new: '_AddonModLabel' }, + { old: /_mmaModLesson/g, new: '_AddonModLesson' }, + { old: /_mmaModLti/g, new: '_AddonModLti' }, + { old: /_mmaModPage/g, new: '_AddonModPage' }, + { old: /_mmaModQuiz/g, new: '_AddonModQuiz' }, + { old: /_mmaModResource/g, new: '_AddonModResource' }, + { old: /_mmaModScorm/g, new: '_AddonModScorm' }, + { old: /_mmaModSurvey/g, new: '_AddonModSurvey' }, + { old: /_mmaModUrl/g, new: '_AddonModUrl' }, + { old: /_mmaModWiki/g, new: '_AddonModWiki' }, + { old: /_mmaModWorkshop/g, new: '_AddonModWorkshop' }, + { old: /remoteAddOn_/g, new: 'sitePlugin_' }, ]; - protected template = document.createElement('template'); // A template element to convert HTML to element. + protected template: HTMLTemplateElement = document.createElement('template'); // A template element to convert HTML to element. constructor(private sanitizer: DomSanitizer) { } @@ -200,7 +202,6 @@ export class CoreTextUtilsProvider { * @return Size in human readable format. */ bytesToSize(bytes: number, precision: number = 2): string { - if (typeof bytes == 'undefined' || bytes === null || bytes < 0) { return Translate.instance.instant('core.notapplicable'); } @@ -449,9 +450,8 @@ export class CoreTextUtilsProvider { * @param courseId Course ID the text belongs to. It can be used to improve performance with filters. * @deprecated since 3.8.3. Please use viewText instead. */ - expandText(title: string, text: string, component?: string, componentId?: string | number, files?: any[], + expandText(title: string, text: string, component?: string, componentId?: string | number, files?: CoreWSExternalFile[], filter?: boolean, contextLevel?: string, instanceId?: number, courseId?: number): void { - return this.viewText(title, text, { component, componentId, @@ -531,12 +531,12 @@ export class CoreTextUtilsProvider { * @param files Files to extract the URL from. They need to have the URL in a 'url' or 'fileurl' attribute. * @return Pluginfile URL, undefined if no files found. */ - getTextPluginfileUrl(files: any[]): string { + getTextPluginfileUrl(files: CoreWSExternalFile[]): string { if (files && files.length) { - const fileURL = files[0].url || files[0].fileurl; + const url = files[0].fileurl; // Remove text after last slash (encoded or not). - return fileURL.substr(0, Math.max(fileURL.lastIndexOf('/'), fileURL.lastIndexOf('%2F'))); + return url.substr(0, Math.max(url.lastIndexOf('/'), url.lastIndexOf('%2F'))); } return undefined; @@ -610,13 +610,17 @@ export class CoreTextUtilsProvider { * @param data Object to be checked. * @return If the data has any long Unicode char on it. */ - hasUnicodeData(data: object): boolean { + hasUnicodeData(data: Record): boolean { for (const el in data) { if (typeof data[el] == 'object') { - if (this.hasUnicodeData(data[el])) { + if (this.hasUnicodeData(data[el] as Record)) { return true; } - } else if (typeof data[el] == 'string' && this.hasUnicode(data[el])) { + + continue; + } + + if (typeof data[el] == 'string' && this.hasUnicode(data[el] as string)) { return true; } } @@ -628,17 +632,17 @@ export class CoreTextUtilsProvider { * Same as Javascript's JSON.parse, but it will handle errors. * * @param json JSON text. - * @param defaultValue Default value t oreturn if the parse fails. Defaults to the original value. + * @param defaultValue Default value to return if the parse fails. Defaults to the original value. * @param logErrorFn An error to call with the exception to log the error. If not supplied, no error. * @return JSON parsed as object or what it gets. */ - parseJSON(json: string, defaultValue?: any, logErrorFn?: (error?: any) => void): any { + parseJSON(json: string, defaultValue?: T, logErrorFn?: (error?: Error) => void): T | string { try { return JSON.parse(json); - } catch (ex) { + } catch (error) { // Error, log the error if needed. if (logErrorFn) { - logErrorFn(ex); + logErrorFn(error); } } @@ -675,7 +679,7 @@ export class CoreTextUtilsProvider { return ''; } - return text.replace(/[#:\/\?\\]+/g, '_'); + return text.replace(/[#:/?\\]+/g, '_'); } /** @@ -700,7 +704,7 @@ export class CoreTextUtilsProvider { * @param files Files to extract the pluginfile URL from. They need to have the URL in a url or fileurl attribute. * @return Treated text. */ - replacePluginfileUrls(text: string, files: any[]): string { + replacePluginfileUrls(text: string, files: CoreWSExternalFile[]): string { if (text && typeof text == 'string') { const fileURL = this.getTextPluginfileUrl(files); if (fileURL) { @@ -718,7 +722,7 @@ export class CoreTextUtilsProvider { * @param files Files to extract the pluginfile URL from. They need to have the URL in a url or fileurl attribute. * @return Treated text. */ - restorePluginfileUrls(text: string, files: any[]): string { + restorePluginfileUrls(text: string, files: CoreWSExternalFile[]): string { if (text && typeof text == 'string') { const fileURL = this.getTextPluginfileUrl(files); if (fileURL) { @@ -804,7 +808,6 @@ export class CoreTextUtilsProvider { /** * Replace text within a portion of a string. Equivalent to PHP's substr_replace. - * Credits to http://locutus.io/php/strings/substr_replace/ * * @param str The string to treat. * @param replace The value to put inside the string. @@ -814,22 +817,7 @@ export class CoreTextUtilsProvider { * @return Treated string. */ substrReplace(str: string, replace: string, start: number, length?: number): string { - length = typeof length != 'undefined' ? length : str.length; - - if (start < 0) { - start = start + str.length; - } - - if (length < 0) { - length = length + str.length - start; - } - - return [ - str.slice(0, start), - replace.substr(0, length), - replace.slice(length), - str.slice(start + length) - ].join(''); + return Locutus.substrReplace(str, replace, start, length); } /** @@ -867,14 +855,14 @@ export class CoreTextUtilsProvider { return CoreLang.instance.getCurrentLanguage().then((language) => { // Match the current language. const anyLangRegEx = /<(?:lang|span)[^>]+lang="[a-zA-Z0-9_-]+"[^>]*>(.*?)<\/(?:lang|span)>/g; - let currentLangRegEx = new RegExp('<(?:lang|span)[^>]+lang="' + language + '"[^>]*>(.*?)<\/(?:lang|span)>', 'g'); + let currentLangRegEx = new RegExp('<(?:lang|span)[^>]+lang="' + language + '"[^>]*>(.*?)', 'g'); if (!text.match(currentLangRegEx)) { // Current lang not found. Try to find the first language. const matches = text.match(anyLangRegEx); if (matches && matches[0]) { language = matches[0].match(/lang="([a-zA-Z0-9_-]+)"/)[1]; - currentLangRegEx = new RegExp('<(?:lang|span)[^>]+lang="' + language + '"[^>]*>(.*?)<\/(?:lang|span)>', 'g'); + currentLangRegEx = new RegExp('<(?:lang|span)[^>]+lang="' + language + '"[^>]*>(.*?)', 'g'); } else { // No multi-lang tag found, stop. return text; @@ -915,221 +903,12 @@ export class CoreTextUtilsProvider { /** * Unserialize Array from PHP. - * Taken from: https://github.com/kvz/locutus/blob/master/src/php/var/unserialize.js * * @param data String to unserialize. - * @param logErrorFn An error to call with the exception to log the error. If not supplied, no error. * @return Unserialized data. */ - unserialize(data: string, logErrorFn?: (error?: string) => void): any { - // Discuss at: http://locutus.io/php/unserialize/ - // Original by: Arpad Ray (mailto:arpad@php.net) - // Improved by: Pedro Tainha (http://www.pedrotainha.com) - // Improved by: Kevin van Zonneveld (http://kvz.io) - // Improved by: Kevin van Zonneveld (http://kvz.io) - // Improved by: Chris - // Improved by: James - // Improved by: Le Torbi - // Improved by: Eli Skeggs - // Bugfixed by: dptr1988 - // Bugfixed by: Kevin van Zonneveld (http://kvz.io) - // Bugfixed by: Brett Zamir (http://brett-zamir.me) - // Bugfixed by: philippsimon (https://github.com/philippsimon/) - // Revised by: d3x - // Input by: Brett Zamir (http://brett-zamir.me) - // Input by: Martin (http://www.erlenwiese.de/) - // Input by: kilops - // Input by: Jaroslaw Czarniak - // Input by: lovasoa (https://github.com/lovasoa/) - // Note 1: We feel the main purpose of this function should be - // Note 1: to ease the transport of data between php & js - // Note 1: Aiming for PHP-compatibility, we have to translate objects to arrays - // Example 1: unserialize('a:3:{i:0;s:5:"Kevin";i:1;s:3:"van";i:2;s:9:"Zonneveld";}') - // Returns 1: ['Kevin', 'van', 'Zonneveld'] - // Example 2: unserialize('a:2:{s:9:"firstName";s:5:"Kevin";s:7:"midName";s:3:"van";}') - // Returns 2: {firstName: 'Kevin', midName: 'van'} - // Example 3: unserialize('a:3:{s:2:"ü";s:2:"ü";s:3:"四";s:3:"四";s:4:"𠜎";s:4:"𠜎";}') - // Returns 3: {'ü': 'ü', '四': '四', '𠜎': '𠜎'} - - const utf8Overhead = (str: string): number => { - let s = str.length; - - for (let i = str.length - 1; i >= 0; i--) { - const code = str.charCodeAt(i); - if (code > 0x7f && code <= 0x7ff) { - s++; - } else if (code > 0x7ff && code <= 0xffff) { - s += 2; - } - // Trail surrogate. - if (code >= 0xDC00 && code <= 0xDFFF) { - i--; - } - } - - return s - 1; - }; - - const error = (type: string, msg: string): void => { - if (logErrorFn) { - logErrorFn(type + msg); - } - }; - - const readUntil = (data: string, offset: number, stopchr: string): Array => { - let i = 2; - const buf = []; - let chr = data.slice(offset, offset + 1); - - while (chr !== stopchr) { - if ((i + offset) > data.length) { - error('Error', 'Invalid'); - } - buf.push(chr); - chr = data.slice(offset + (i - 1), offset + i); - i += 1; - } - - return [buf.length, buf.join('')]; - }; - - const readChrs = (data: string, offset: number, length: number): Array => { - let chr; - const buf = []; - - for (let i = 0; i < length; i++) { - chr = data.slice(offset + (i - 1), offset + i); - buf.push(chr); - length -= utf8Overhead(chr); - } - - return [buf.length, buf.join('')]; - }; - - const _unserialize = (data: string, offset: number): any => { - let dtype, - dataoffset, - keyandchrs, - keys, - contig, - length, - array, - readdata, - readData, - ccount, - stringlength, - i, - key, - kprops, - kchrs, - vprops, - vchrs, - value, - chrs = 0, - typeconvert = (x: any): any => { - return x; - }; - - if (!offset) { - offset = 0; - } - dtype = (data.slice(offset, offset + 1)).toLowerCase(); - - dataoffset = offset + 2; - - switch (dtype) { - case 'i': - typeconvert = (x: any): number => { - return parseInt(x, 10); - }; - readData = readUntil(data, dataoffset, ';'); - chrs = readData[0]; - readdata = readData[1]; - dataoffset += chrs + 1; - break; - case 'b': - typeconvert = (x: any): boolean => { - return parseInt(x, 10) !== 0; - }; - readData = readUntil(data, dataoffset, ';'); - chrs = readData[0]; - readdata = readData[1]; - dataoffset += chrs + 1; - break; - case 'd': - typeconvert = (x: any): number => { - return parseFloat(x); - }; - readData = readUntil(data, dataoffset, ';'); - chrs = readData[0]; - readdata = readData[1]; - dataoffset += chrs + 1; - break; - case 'n': - readdata = null; - break; - case 's': - ccount = readUntil(data, dataoffset, ':'); - chrs = ccount[0]; - stringlength = ccount[1]; - dataoffset += chrs + 2; - - readData = readChrs(data, dataoffset + 1, parseInt(stringlength, 10)); - chrs = readData[0]; - readdata = readData[1]; - dataoffset += chrs + 2; - if (chrs !== parseInt(stringlength, 10) && chrs !== readdata.length) { - error('SyntaxError', 'String length mismatch'); - } - break; - case 'a': - readdata = {}; - - keyandchrs = readUntil(data, dataoffset, ':'); - chrs = keyandchrs[0]; - keys = keyandchrs[1]; - dataoffset += chrs + 2; - - length = parseInt(keys, 10); - contig = true; - - for (let i = 0; i < length; i++) { - kprops = _unserialize(data, dataoffset); - kchrs = kprops[1]; - key = kprops[2]; - dataoffset += kchrs; - - vprops = _unserialize(data, dataoffset); - vchrs = vprops[1]; - value = vprops[2]; - dataoffset += vchrs; - - if (key !== i) { - contig = false; - } - - readdata[key] = value; - } - - if (contig) { - array = new Array(length); - for (i = 0; i < length; i++) { - array[i] = readdata[i]; - } - readdata = array; - } - - dataoffset += 1; - break; - default: - error('SyntaxError', 'Unknown / Unhandled data type(s): ' + dtype); - break; - } - - return [dtype, dataoffset - offset, typeconvert(readdata)]; - }; - - return _unserialize((data + ''), 0)[2]; + unserialize(data: string): T { + return Locutus.unserialize(data); } /** @@ -1138,16 +917,13 @@ export class CoreTextUtilsProvider { * @param title Title of the new state. * @param text Content of the text to be expanded. * @param component Component to link the embedded files to. - * @param componentId An ID to use in conjunction with the component. - * @param files List of files to display along with the text. - * @param filter Whether the text should be filtered. - * @param contextLevel The context level. - * @param instanceId The instance ID related to the context. - * @param courseId Course ID the text belongs to. It can be used to improve performance with filters. + * @param options Options. */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars viewText(title: string, text: string, options?: CoreTextUtilsViewTextOptions): void { // @todo } + } /** @@ -1156,7 +932,7 @@ export class CoreTextUtilsProvider { export type CoreTextUtilsViewTextOptions = { component?: string; // Component to link the embedded files to. componentId?: string | number; // An ID to use in conjunction with the component. - files?: any[]; // List of files to display along with the text. + files?: CoreWSExternalFile[]; // List of files to display along with the text. filter?: boolean; // Whether the text should be filtered. contextLevel?: string; // The context level. instanceId?: number; // The instance ID related to the context. diff --git a/src/app/singletons/locutus.ts b/src/app/singletons/locutus.ts new file mode 100644 index 000000000..0cc32e5c7 --- /dev/null +++ b/src/app/singletons/locutus.ts @@ -0,0 +1,447 @@ +/* eslint-disable */ + +/** + * Original code taken from https://github.com/kvz/locutus + */ + +function initCache () { + const store = [] + // cache only first element, second is length to jump ahead for the parser + const cache = function cache (value) { + store.push(value[0]) + return value + } + + cache.get = (index) => { + if (index >= store.length) { + throw RangeError(`Can't resolve reference ${index + 1}`) + } + + return store[index] + } + + return cache +} + +function expectType (str, cache) { + const types = /^(?:N(?=;)|[bidsSaOCrR](?=:)|[^:]+(?=:))/g + const type = (types.exec(str) || [])[0] + + if (!type) { + throw SyntaxError('Invalid input: ' + str) + } + + switch (type) { + case 'N': + return cache([ null, 2 ]) + case 'b': + return cache(expectBool(str)) + case 'i': + return cache(expectInt(str)) + case 'd': + return cache(expectFloat(str)) + case 's': + return cache(expectString(str)) + case 'S': + return cache(expectEscapedString(str)) + case 'a': + return expectArray(str, cache) + case 'O': + return expectObject(str, cache) + case 'C': + return expectClass(str, cache) + case 'r': + case 'R': + return expectReference(str, cache) + default: + throw SyntaxError(`Invalid or unsupported data type: ${type}`) + } +} + +function expectBool (str) { + const reBool = /^b:([01]);/ + const [ match, boolMatch ] = reBool.exec(str) || [] + + if (!boolMatch) { + throw SyntaxError('Invalid bool value, expected 0 or 1') + } + + return [ boolMatch === '1', match.length ] +} + +function expectInt (str) { + const reInt = /^i:([+-]?\d+);/ + const [ match, intMatch ] = reInt.exec(str) || [] + + if (!intMatch) { + throw SyntaxError('Expected an integer value') + } + + return [ parseInt(intMatch, 10), match.length ] +} + +function expectFloat (str) { + const reFloat = /^d:(NAN|-?INF|(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]\d+)?);/ + const [ match, floatMatch ] = reFloat.exec(str) || [] + + if (!floatMatch) { + throw SyntaxError('Expected a float value') + } + + let floatValue + + switch (floatMatch) { + case 'NAN': + floatValue = Number.NaN + break + case '-INF': + floatValue = Number.NEGATIVE_INFINITY + break + case 'INF': + floatValue = Number.POSITIVE_INFINITY + break + default: + floatValue = parseFloat(floatMatch) + break + } + + return [ floatValue, match.length ] +} + +function readBytes (str, len, escapedString = false) { + let bytes = 0 + let out = '' + let c = 0 + const strLen = str.length + let wasHighSurrogate = false + let escapedChars = 0 + + while (bytes < len && c < strLen) { + let chr = str.charAt(c) + const code = chr.charCodeAt(0) + const isHighSurrogate = code >= 0xd800 && code <= 0xdbff + const isLowSurrogate = code >= 0xdc00 && code <= 0xdfff + + if (escapedString && chr === '\\') { + chr = String.fromCharCode(parseInt(str.substr(c + 1, 2), 16)) + escapedChars++ + + // each escaped sequence is 3 characters. Go 2 chars ahead. + // third character will be jumped over a few lines later + c += 2 + } + + c++ + + bytes += isHighSurrogate || (isLowSurrogate && wasHighSurrogate) + // if high surrogate, count 2 bytes, as expectation is to be followed by low surrogate + // if low surrogate preceded by high surrogate, add 2 bytes + ? 2 + : code > 0x7ff + // otherwise low surrogate falls into this part + ? 3 + : code > 0x7f + ? 2 + : 1 + + // if high surrogate is not followed by low surrogate, add 1 more byte + bytes += wasHighSurrogate && !isLowSurrogate ? 1 : 0 + + out += chr + wasHighSurrogate = isHighSurrogate + } + + return [ out, bytes, escapedChars ] +} + +function expectString (str) { + // PHP strings consist of one-byte characters. + // JS uses 2 bytes with possible surrogate pairs. + // Serialized length of 2 is still 1 JS string character + const reStrLength = /^s:(\d+):"/g // also match the opening " char + const [ match, byteLenMatch ] = reStrLength.exec(str) || [] + + if (!match) { + throw SyntaxError('Expected a string value') + } + + const len = parseInt(byteLenMatch, 10) + + str = str.substr(match.length) + + let [ strMatch, bytes ] = readBytes(str, len) + + if (bytes !== len) { + throw SyntaxError(`Expected string of ${len} bytes, but got ${bytes}`) + } + + str = str.substr((strMatch as string).length) + + // strict parsing, match closing "; chars + if (!str.startsWith('";')) { + throw SyntaxError('Expected ";') + } + + return [ strMatch, match.length + (strMatch as string).length + 2 ] // skip last "; +} + +function expectEscapedString (str) { + const reStrLength = /^S:(\d+):"/g // also match the opening " char + const [ match, strLenMatch ] = reStrLength.exec(str) || [] + + if (!match) { + throw SyntaxError('Expected an escaped string value') + } + + const len = parseInt(strLenMatch, 10) + + str = str.substr(match.length) + + let [ strMatch, bytes, escapedChars ] = readBytes(str, len, true) + + if (bytes !== len) { + throw SyntaxError(`Expected escaped string of ${len} bytes, but got ${bytes}`) + } + + str = str.substr((strMatch as string).length + (escapedChars as number) * 2) + + // strict parsing, match closing "; chars + if (!str.startsWith('";')) { + throw SyntaxError('Expected ";') + } + + return [ strMatch, match.length + (strMatch as string).length + 2 ] // skip last "; +} + +function expectKeyOrIndex (str) { + try { + return expectString(str) + } catch (err) {} + + try { + return expectEscapedString(str) + } catch (err) {} + + try { + return expectInt(str) + } catch (err) { + throw SyntaxError('Expected key or index') + } +} + +function expectObject (str, cache) { + // O::"class name"::{} + // O:8:"stdClass":2:{s:3:"foo";s:3:"bar";s:3:"bar";s:3:"baz";} + const reObjectLiteral = /^O:(\d+):"([^"]+)":(\d+):\{/ + const [ objectLiteralBeginMatch, /* classNameLengthMatch */, className, propCountMatch ] = reObjectLiteral.exec(str) || [] + + if (!objectLiteralBeginMatch) { + throw SyntaxError('Invalid input') + } + + if (className !== 'stdClass') { + throw SyntaxError(`Unsupported object type: ${className}`) + } + + let totalOffset = objectLiteralBeginMatch.length + + const propCount = parseInt(propCountMatch, 10) + const obj = {} + cache([obj]) + + str = str.substr(totalOffset) + + for (let i = 0; i < propCount; i++) { + const prop = expectKeyOrIndex(str) + str = str.substr(prop[1]) + totalOffset += prop[1] as number + + const value = expectType(str, cache) + str = str.substr(value[1]) + totalOffset += value[1] + + obj[prop[0]] = value[0] + } + + // strict parsing, expect } after object literal + if (str.charAt(0) !== '}') { + throw SyntaxError('Expected }') + } + + return [ obj, totalOffset + 1 ] // skip final } +} + +function expectClass (str, cache) { + // can't be well supported, because requires calling eval (or similar) + // in order to call serialized constructor name + // which is unsafe + // or assume that constructor is defined in global scope + // but this is too much limiting + throw Error('Not yet implemented') +} + +function expectReference (str, cache) { + const reRef = /^[rR]:([1-9]\d*);/ + const [ match, refIndex ] = reRef.exec(str) || [] + + if (!match) { + throw SyntaxError('Expected reference value') + } + + return [ cache.get(parseInt(refIndex, 10) - 1), match.length ] +} + +function expectArray (str, cache) { + const reArrayLength = /^a:(\d+):{/ + const [ arrayLiteralBeginMatch, arrayLengthMatch ] = reArrayLength.exec(str) || [] + + if (!arrayLengthMatch) { + throw SyntaxError('Expected array length annotation') + } + + str = str.substr(arrayLiteralBeginMatch.length) + + const array = expectArrayItems(str, parseInt(arrayLengthMatch, 10), cache) + + // strict parsing, expect closing } brace after array literal + if (str.charAt(array[1]) !== '}') { + throw SyntaxError('Expected }') + } + + return [ array[0], arrayLiteralBeginMatch.length + (array[1] as number) + 1 ] // jump over } +} + +function expectArrayItems (str, expectedItems = 0, cache) { + let key + let hasStringKeys = false + let item + let totalOffset = 0 + let items = [] + cache([items]) + + for (let i = 0; i < expectedItems; i++) { + key = expectKeyOrIndex(str) + + // this is for backward compatibility with previous implementation + if (!hasStringKeys) { + hasStringKeys = (typeof key[0] === 'string') + } + + str = str.substr(key[1]) + totalOffset += key[1] + + // references are resolved immediately, so if duplicate key overwrites previous array index + // the old value is anyway resolved + // fixme: but next time the same reference should point to the new value + item = expectType(str, cache) + str = str.substr(item[1]) + totalOffset += item[1] + + items[key[0]] = item[0] + } + + // this is for backward compatibility with previous implementation + if (hasStringKeys) { + items = Object.assign({}, items) + } + + return [ items, totalOffset ] +} + +function unserialize (str) { + // discuss at: https://locutus.io/php/unserialize/ + // original by: Arpad Ray (mailto:arpad@php.net) + // improved by: Pedro Tainha (https://www.pedrotainha.com) + // improved by: Kevin van Zonneveld (https://kvz.io) + // improved by: Kevin van Zonneveld (https://kvz.io) + // improved by: Chris + // improved by: James + // improved by: Le Torbi + // improved by: Eli Skeggs + // bugfixed by: dptr1988 + // bugfixed by: Kevin van Zonneveld (https://kvz.io) + // bugfixed by: Brett Zamir (https://brett-zamir.me) + // bugfixed by: philippsimon (https://github.com/philippsimon/) + // revised by: d3x + // input by: Brett Zamir (https://brett-zamir.me) + // input by: Martin (https://www.erlenwiese.de/) + // input by: kilops + // input by: Jaroslaw Czarniak + // input by: lovasoa (https://github.com/lovasoa/) + // improved by: Rafał Kukawski + // reimplemented by: Rafał Kukawski + // note 1: We feel the main purpose of this function should be + // note 1: to ease the transport of data between php & js + // note 1: Aiming for PHP-compatibility, we have to translate objects to arrays + // example 1: unserialize('a:3:{i:0;s:5:"Kevin";i:1;s:3:"van";i:2;s:9:"Zonneveld";}') + // returns 1: ['Kevin', 'van', 'Zonneveld'] + // example 2: unserialize('a:2:{s:9:"firstName";s:5:"Kevin";s:7:"midName";s:3:"van";}') + // returns 2: {firstName: 'Kevin', midName: 'van'} + // example 3: unserialize('a:3:{s:2:"ü";s:2:"ü";s:3:"四";s:3:"四";s:4:"𠜎";s:4:"𠜎";}') + // returns 3: {'ü': 'ü', '四': '四', '𠜎': '𠜎'} + // example 4: unserialize(undefined) + // returns 4: false + // example 5: unserialize('O:8:"stdClass":1:{s:3:"foo";b:1;}') + // returns 5: { foo: true } + // example 6: unserialize('a:2:{i:0;N;i:1;s:0:"";}') + // returns 6: [null, ""] + // example 7: unserialize('S:7:"\\65\\73\\63\\61\\70\\65\\64";') + // returns 7: 'escaped' + + try { + if (typeof str !== 'string') { + return false + } + + return expectType(str, initCache())[0] + } catch (err) { + console.error(err) + return false + } +} + +function substr_replace (str, replace, start, length) { // eslint-disable-line camelcase + // discuss at: https://locutus.io/php/substr_replace/ + // original by: Brett Zamir (https://brett-zamir.me) + // example 1: substr_replace('ABCDEFGH:/MNRPQR/', 'bob', 0) + // returns 1: 'bob' + // example 2: var $var = 'ABCDEFGH:/MNRPQR/' + // example 2: substr_replace($var, 'bob', 0, $var.length) + // returns 2: 'bob' + // example 3: substr_replace('ABCDEFGH:/MNRPQR/', 'bob', 0, 0) + // returns 3: 'bobABCDEFGH:/MNRPQR/' + // example 4: substr_replace('ABCDEFGH:/MNRPQR/', 'bob', 10, -1) + // returns 4: 'ABCDEFGH:/bob/' + // example 5: substr_replace('ABCDEFGH:/MNRPQR/', 'bob', -7, -1) + // returns 5: 'ABCDEFGH:/bob/' + // example 6: substr_replace('ABCDEFGH:/MNRPQR/', '', 10, -1) + // returns 6: 'ABCDEFGH://' + + if (start < 0) { + // start position in str + start = start + str.length + } + length = length !== undefined ? length : str.length + if (length < 0) { + length = length + str.length - start + } + + return [ + str.slice(0, start), + replace.substr(0, length), + replace.slice(length), + str.slice(start + length) + ].join('') +} + +export class Locutus { + + static unserialize(data: string): T { + return unserialize(data); + } + + static substrReplace(str: string, replace: string, start: number, length?: number): string { + return substr_replace(str, replace, start, length); + } + +};