diff --git a/packages/text/src/markdown/serializer.ts b/packages/text/src/markdown/serializer.ts
index b3c7b9695c..a78d8572db 100644
--- a/packages/text/src/markdown/serializer.ts
+++ b/packages/text/src/markdown/serializer.ts
@@ -466,7 +466,11 @@ export class MarkdownState implements IState {
for (let i = 0; i < len; i++) {
const mark = state.marks[i]
- if (!this.marks[mark.type].mixable) break
+ const mm = this.marks[mark.type]
+ if (mm == null) {
+ break
+ }
+ if (!mm.mixable) break
this.reorderMixableMark(state, mark, i, len)
}
}
@@ -735,6 +739,9 @@ export class MarkdownState implements IState {
let value = mark.attrs?.marker
if (value === undefined) {
const info = this.marks[mark.type]
+ if (info == null) {
+ throw new Error(`No info for mark ${mark.type}`)
+ }
value = open ? info.open : info.close
}
return typeof value === 'string' ? value : value(this, mark, parent, index) ?? ''
diff --git a/plugins/hr-assets/lang/ru.json b/plugins/hr-assets/lang/ru.json
index fe24bea6db..323e6f1934 100644
--- a/plugins/hr-assets/lang/ru.json
+++ b/plugins/hr-assets/lang/ru.json
@@ -27,11 +27,11 @@
"NoEmployeesInDepartment": "Нет сотрудников в выбранном департаменте",
"Vacation": "Отпуск",
"Sick": "Больничный",
- "PTO": "Отпуск",
- "PTOs": "Отпускных дней",
+ "PTO": "Отгул(PTO)",
+ "PTOs": "Отгулов",
"Remote": "Удаленно",
"Overtime": "Переработка",
- "PTO2": "Отпуск/2",
+ "PTO2": "Отгул(PTO)/2",
"Overtime2": "Переработка/2",
"EditRequest": "Редактировать {type}",
"EditRequestType": "Редактировать тип",
diff --git a/plugins/tracker-resources/src/components/issues/timereport/TimePresenter.svelte b/plugins/tracker-resources/src/components/issues/timereport/TimePresenter.svelte
index cf31db36b4..9364b42829 100644
--- a/plugins/tracker-resources/src/components/issues/timereport/TimePresenter.svelte
+++ b/plugins/tracker-resources/src/components/issues/timereport/TimePresenter.svelte
@@ -13,9 +13,9 @@
// limitations under the License.
-->
@@ -63,13 +64,7 @@
class:link={kind === 'link'}
class:fs-bold={accent}
on:click
- use:tooltip={{
- component: Label,
- props: {
- label: tracker.string.TimeSpendHours,
- params: { value }
- }
- }}
+ use:tooltip={{ label: getEmbeddedLabel(label) }}
>
{label}
diff --git a/pods/front/src/__start.ts b/pods/front/src/__start.ts
index 9a19915188..69c05de9a1 100644
--- a/pods/front/src/__start.ts
+++ b/pods/front/src/__start.ts
@@ -48,5 +48,6 @@ startFront(metricsContext, {
AI_URL: process.env.AI_URL,
TELEGRAM_BOT_URL: process.env.TELEGRAM_BOT_URL,
STATS_URL: process.env.STATS_API ?? process.env.STATS_URL,
- BACKUP_URL: process.env.BACKUP_URL
+ BACKUP_URL: process.env.BACKUP_URL,
+ TRANSACTOR_OVERRIDE: process.env.TRANSACTOR_OVERRIDE
})
diff --git a/server/server/src/utils.ts b/server/server/src/utils.ts
index 6475799706..84cddb0fe6 100644
--- a/server/server/src/utils.ts
+++ b/server/server/src/utils.ts
@@ -78,6 +78,7 @@ export function processRequest (
}
}
}
+
export function sendResponse (
ctx: MeasureContext,
session: Session,
diff --git a/services/github/pod-github/src/platform.ts b/services/github/pod-github/src/platform.ts
index b39270a77f..05d05bbb11 100644
--- a/services/github/pod-github/src/platform.ts
+++ b/services/github/pod-github/src/platform.ts
@@ -11,6 +11,7 @@ import core, {
ClientConnectEvent,
DocumentUpdate,
isActiveMode,
+ isDeletingMode,
MeasureContext,
RateLimiter,
TimeRateLimiter,
@@ -714,35 +715,37 @@ export class PlatformWorker {
return Array.from(workspaces)
}
- async checkWorkspaceIsActive (token: string, workspace: string): Promise {
+ async checkWorkspaceIsActive (
+ token: string,
+ workspace: string
+ ): Promise<{ workspaceInfo: WorkspaceInfoWithStatus | undefined, needRecheck: boolean }> {
let workspaceInfo: WorkspaceInfoWithStatus | undefined
try {
workspaceInfo = await getAccountClient(token).getWorkspaceInfo(true)
} catch (err: any) {
this.ctx.error('Workspace not found:', { workspace })
- return
+ return { workspaceInfo: undefined, needRecheck: false }
}
if (workspaceInfo?.uuid === undefined) {
this.ctx.error('No workspace exists for workspaceId', { workspace })
- return
+ return { workspaceInfo: undefined, needRecheck: false }
+ }
+ if (workspaceInfo?.isDisabled === true || isDeletingMode(workspaceInfo?.mode)) {
+ this.ctx.warn('Workspace is disabled', { workspace })
+ return { workspaceInfo: undefined, needRecheck: false }
}
if (!isActiveMode(workspaceInfo?.mode)) {
- this.ctx.warn('Workspace is in maitenance, skipping for now.', { workspace })
- return
- }
- if (workspaceInfo?.isDisabled === true) {
- this.ctx.warn('Workspace is disabled', { workspace })
- return
+ this.ctx.warn('Workspace is in maitenance, skipping for now.', { workspace, mode: workspaceInfo?.mode })
+ return { workspaceInfo: undefined, needRecheck: true }
}
const lastVisit = (Date.now() - (workspaceInfo.lastVisit ?? 0)) / (3600 * 24 * 1000) // In days
if (config.WorkspaceInactivityInterval > 0 && lastVisit > config.WorkspaceInactivityInterval) {
this.ctx.warn('Workspace is inactive for too long, skipping for now.', { workspace })
- return
+ return { workspaceInfo: undefined, needRecheck: true }
}
-
- return workspaceInfo
+ return { workspaceInfo, needRecheck: true }
}
private async checkWorkspaces (): Promise {
@@ -779,9 +782,11 @@ export class PlatformWorker {
}
await rateLimiter.add(async () => {
const token = generateToken(systemAccountUuid, workspace, { service: 'github', mode: 'github' })
- const workspaceInfo = await this.checkWorkspaceIsActive(token, workspace)
+ const { workspaceInfo, needRecheck } = await this.checkWorkspaceIsActive(token, workspace)
if (workspaceInfo === undefined) {
- errors++
+ if (needRecheck) {
+ errors++
+ }
return
}
try {
diff --git a/services/github/pod-github/src/sync/issueBase.ts b/services/github/pod-github/src/sync/issueBase.ts
index 27793567b5..ccd645ddee 100644
--- a/services/github/pod-github/src/sync/issueBase.ts
+++ b/services/github/pod-github/src/sync/issueBase.ts
@@ -499,7 +499,15 @@ export abstract class IssueSyncManagerBase {
return (pField.node.options ?? []).find((it) => it.id === field.optionId)
}
- findOptionId (container: ContainerFocus, fieldId: string, value: string, target: IssueSyncTarget): string | undefined {
+ findOptionId (
+ container: ContainerFocus,
+ fieldId: string,
+ value: string | null,
+ target: IssueSyncTarget
+ ): string | undefined {
+ if (value == null) {
+ return
+ }
const structure = container.container.projectStructure.get(target.target._id)
if (structure === undefined) {
return
@@ -508,7 +516,7 @@ export abstract class IssueSyncManagerBase {
if (pField === undefined) {
return undefined
}
- return (pField.node.options ?? []).find((it) => it.name.toLowerCase() === value.toLowerCase())?.id
+ return (pField.node.options ?? []).find((it) => it.name?.toLowerCase() === value.toLowerCase())?.id
}
async toPlatformField (
diff --git a/services/github/pod-github/src/sync/projects.ts b/services/github/pod-github/src/sync/projects.ts
index 088e6263a2..9007ea458b 100644
--- a/services/github/pod-github/src/sync/projects.ts
+++ b/services/github/pod-github/src/sync/projects.ts
@@ -346,7 +346,7 @@ export class ProjectsSyncManager implements DocSyncManager {
derivedClient: TxOperations,
deleteExisting: boolean
): Promise {
- return false
+ return true
}
async externalSync (
diff --git a/services/github/pod-github/src/sync/utils.ts b/services/github/pod-github/src/sync/utils.ts
index a36c72c64c..af1756c435 100644
--- a/services/github/pod-github/src/sync/utils.ts
+++ b/services/github/pod-github/src/sync/utils.ts
@@ -127,6 +127,9 @@ export async function getSinceRaw (
export function gqlp (params: Record): string {
let result = ''
let first = true
+ function escape (str: string): string {
+ return str.replace(/"/g, '\\"')
+ }
for (const [k, v] of Object.entries(params)) {
if (v !== undefined) {
if (!first) {
@@ -136,9 +139,9 @@ export function gqlp (params: Record `"${it}"`).join(', ')}]`
+ result += `${k}: [${v.map((it) => `"${escape(it)}"`).join(', ')}]`
} else {
- result += `${k}: "${v}"`
+ result += `${k}: "${escape(v)}"`
}
}
}
diff --git a/services/github/pod-github/src/worker.ts b/services/github/pod-github/src/worker.ts
index 5c8554bb42..b459a15230 100644
--- a/services/github/pod-github/src/worker.ts
+++ b/services/github/pod-github/src/worker.ts
@@ -238,7 +238,10 @@ export class GithubWorker implements IntegrationManager {
}
}
- async getAccountU (user: User): Promise {
+ async getAccountU (user?: User): Promise {
+ if (user == null) {
+ return undefined
+ }
return await this.getAccount({
id: user.node_id,
login: user.login,
@@ -1126,38 +1129,41 @@ export class GithubWorker implements IntegrationManager {
this.ctx.error('Failed to perform full sync', { error: err })
})
}
-
- const { projects, repositories } = await this.collectActiveProjects()
- if (projects.length === 0 && repositories.length === 0) {
- await this.waitChanges()
- continue
- }
-
- // Check if we have documents with external sync request's pending.
- const hadExternalChanges = await this.performExternalSync(
- projects,
- repositories,
- 'externalVersion',
- githubExternalSyncVersion
- )
- const hadSyncChanges = await this.performSync(projects, repositories)
-
- // Perform derived operations
- // Sync derived external data, like pull request reviews, files etc.
- const hadDerivedChanges = await this.performExternalSync(
- projects,
- repositories,
- 'derivedVersion',
- githubDerivedSyncVersion
- )
-
- if (!hadExternalChanges && !hadSyncChanges && !hadDerivedChanges) {
- if (this.previousWait !== 0) {
- this.ctx.info('Wait for changes:', { previousWait: this.previousWait, workspace: this.workspace.uuid })
- this.previousWait = 0
+ try {
+ const { projects, repositories } = await this.collectActiveProjects()
+ if (projects.length === 0 && repositories.length === 0) {
+ await this.waitChanges()
+ continue
}
- // Wait until some sync documents will be modified, updated.
- await this.waitChanges()
+
+ // Check if we have documents with external sync request's pending.
+ const hadExternalChanges = await this.performExternalSync(
+ projects,
+ repositories,
+ 'externalVersion',
+ githubExternalSyncVersion
+ )
+ const hadSyncChanges = await this.performSync(projects, repositories)
+
+ // Perform derived operations
+ // Sync derived external data, like pull request reviews, files etc.
+ const hadDerivedChanges = await this.performExternalSync(
+ projects,
+ repositories,
+ 'derivedVersion',
+ githubDerivedSyncVersion
+ )
+
+ if (!hadExternalChanges && !hadSyncChanges && !hadDerivedChanges) {
+ if (this.previousWait !== 0) {
+ this.ctx.info('Wait for changes:', { previousWait: this.previousWait, workspace: this.workspace.url })
+ this.previousWait = 0
+ }
+ // Wait until some sync documents will be modified, updated.
+ await this.waitChanges()
+ }
+ } catch (err: any) {
+ this.ctx.error('failed to perform sync', { err, workspace: this.workspace.url })
}
}
}
@@ -1352,25 +1358,30 @@ export class GithubWorker implements IntegrationManager {
const targetProject = await this.client.findOne(github.mixin.GithubProject, {
_id: existing.space as Ref
})
- if (await mapper.handleDelete(existing, info, derivedClient, false, parent)) {
- const h = this._client.getHierarchy()
- await derivedClient.remove(info)
- if (h.hasMixin(existing, github.mixin.GithubIssue)) {
- const mixinData = this._client.getHierarchy().as(existing, github.mixin.GithubIssue)
- await this._client.update(
- mixinData,
- {
- url: '',
- githubNumber: 0,
- repository: '' as Ref
- },
- false,
- Date.now(),
- existing.modifiedBy
- )
+ try {
+ if (await mapper.handleDelete(existing, info, derivedClient, false, parent)) {
+ const h = this._client.getHierarchy()
+ await derivedClient.remove(info)
+ if (h.hasMixin(existing, github.mixin.GithubIssue)) {
+ const mixinData = this._client.getHierarchy().as(existing, github.mixin.GithubIssue)
+ await this._client.update(
+ mixinData,
+ {
+ url: '',
+ githubNumber: 0,
+ repository: '' as Ref
+ },
+ false,
+ Date.now(),
+ existing.modifiedBy
+ )
+ }
+ return
}
- return
+ } catch (err: any) {
+ this.ctx.error('failed to handle delete', { err })
}
+
if (targetProject !== undefined) {
// We need to sync into new project.
await derivedClient.update(info, {
@@ -1386,8 +1397,12 @@ export class GithubWorker implements IntegrationManager {
}
if (info.deleted === true) {
- if (await mapper.handleDelete(existing, info, derivedClient, true)) {
- await derivedClient.remove(info)
+ try {
+ if (await mapper.handleDelete(existing, info, derivedClient, true)) {
+ await derivedClient.remove(info)
+ }
+ } catch (err: any) {
+ this.ctx.error('failed to handle delete', { err })
}
return
}
diff --git a/services/print/pod-print/src/config.ts b/services/print/pod-print/src/config.ts
index 343d5c7c40..2dc7d1ba53 100644
--- a/services/print/pod-print/src/config.ts
+++ b/services/print/pod-print/src/config.ts
@@ -6,15 +6,19 @@ export interface Config {
Port: number
Secret: string
AccountsUrl: string
+ AllowedHostnames: string[]
}
const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined)
const config: Config = (() => {
+ const allowedHostnames = process.env.ALLOWED_HOSTNAMES
+
const params: Partial = {
Port: parseNumber(process.env.PORT) ?? 4005,
Secret: process.env.SECRET,
- AccountsUrl: process.env.ACCOUNTS_URL
+ AccountsUrl: process.env.ACCOUNTS_URL,
+ AllowedHostnames: allowedHostnames == null ? [] : allowedHostnames.split(',')
}
const missingEnv = (Object.keys(params) as Array).filter((key) => params[key] === undefined)
diff --git a/services/print/pod-print/src/main.ts b/services/print/pod-print/src/main.ts
index bf77e34e98..5a6894a2d8 100644
--- a/services/print/pod-print/src/main.ts
+++ b/services/print/pod-print/src/main.ts
@@ -17,7 +17,7 @@ export const main = async (): Promise => {
setupMetadata()
const storageConfig = storageConfigFromEnv()
- const { app, close } = createServer(storageConfig)
+ const { app, close } = createServer(storageConfig, config.AllowedHostnames)
const server = listen(app, config.Port)
const shutdown = (): void => {
diff --git a/services/print/pod-print/src/server.ts b/services/print/pod-print/src/server.ts
index 2b4e6e6d6d..a9cccc6a12 100644
--- a/services/print/pod-print/src/server.ts
+++ b/services/print/pod-print/src/server.ts
@@ -124,9 +124,13 @@ const wrapRequest = (fn: AsyncRequestHandler) => (req: Request, res: Response, n
handleRequest(fn, req, res, next)
}
-export function createServer (storageConfig: StorageConfiguration): { app: Express, close: () => void } {
+export function createServer (
+ storageConfig: StorageConfiguration,
+ allowedHostnames: string[]
+): { app: Express, close: () => void } {
const storageAdapter = buildStorageFromConfig(storageConfig)
const measureCtx = initStatisticsContext('print', {})
+ const whitelistedHostnames = allowedHostnames.length > 0 ? new Set(allowedHostnames) : null
const app = express()
app.use(cors())
@@ -134,9 +138,20 @@ export function createServer (storageConfig: StorageConfiguration): { app: Expre
app.get(
'/print',
- wrapRequest(async (req, res, wsIds) => {
+ wrapRequest(async (req, res, wsIds, token) => {
const rawlink = req.query.link as string
const link = decodeURIComponent(rawlink)
+
+ // Verify that link is from the same host and protocol is among the allowed
+ const url = new URL(link)
+ if (
+ !['http:', 'https:'].includes(url.protocol) ||
+ (whitelistedHostnames != null && !whitelistedHostnames.has(url.hostname))
+ ) {
+ console.error(`Rejected processing unexpected link: ${link}. Token: ${JSON.stringify(token)}`)
+ throw new ApiError(403, 'Cannot process provided link')
+ }
+
const kind = req.query.kind as PrintOptions['kind']
if (kind !== undefined && !validKinds.includes(kind as any)) {
diff --git a/tests/sanity/tests/tracker/tracker.utils.ts b/tests/sanity/tests/tracker/tracker.utils.ts
index 7d87d0f70c..fede06cee5 100644
--- a/tests/sanity/tests/tracker/tracker.utils.ts
+++ b/tests/sanity/tests/tracker/tracker.utils.ts
@@ -294,7 +294,7 @@ export function convertEstimation (estimation: number | string): string {
const days = Math.floor(value / hoursInWorkingDay)
const hours = Math.floor(value % hoursInWorkingDay)
- const minutes = Math.floor((value % 1) * 60)
+ const minutes = Math.round((value % 1) * 60)
const result = [
...(days === 0 ? [] : [`${days}d`]),
...(hours === 0 ? [] : [`${hours}h`]),