/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {NgIf} from '@angular/common';
import {DomSanitizer} from '@angular/platform-browser';
import {
  ChangeDetectionStrategy,
  Component,
  createComponent,
  Directive,
  EnvironmentInjector,
  inject,
  provideZoneChangeDetection,
  TemplateRef,
  Type,
  ViewChild,
  ViewContainerRef,
} from '../../src/core';
import {RuntimeErrorCode} from '../../src/errors';
import {global} from '../../src/util/global';
import {ComponentFixture, TestBed} from '../../testing';

describe('comment node text escaping', () => {
  // see: https://html.spec.whatwg.org/multipage/syntax.html#comments
  [
    '>', // self closing
    '-->', // standard closing
    '--!>', // alternate closing
    '<!-- -->', // embedded comment.
  ].forEach((xssValue) => {
    it(
      'should not be possible to do XSS through comment reflect data when writing: ' + xssValue,
      () => {
        @Component({
          template: `<div>
            <span *ngIf="xssValue"></span>
            <div></div>
          </div>`,
          standalone: false,
        })
        class XSSComp {
          // ngIf serializes the `xssValue` into a comment for debugging purposes.
          xssValue: string = xssValue + '<script>"evil"</script>';
        }

        TestBed.configureTestingModule({declarations: [XSSComp]});
        const fixture = TestBed.createComponent(XSSComp);
        fixture.detectChanges();
        const div = fixture.nativeElement.querySelector('div') as HTMLElement;
        // Serialize into a string to mimic SSR serialization.
        const html = div.innerHTML;
        // This must be escaped or we have XSS.
        expect(html).not.toContain('--><script');
        // Now parse it back into DOM (from string)
        div.innerHTML = html;
        // Verify that we did not accidentally deserialize the `<script>`
        const script = div.querySelector('script');
        expect(script).toBeFalsy();
      },
    );
  });
});

describe('iframe processing', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideZoneChangeDetection()],
    });
  });
  function getErrorMessageRegexp() {
    const errorMessagePart = 'NG0' + Math.abs(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING).toString();
    return new RegExp(errorMessagePart);
  }

  function ensureNoIframePresent(fixture?: ComponentFixture<unknown>) {
    // Note: a `fixture` may not exist in case an error was thrown at creation time.
    const iframe = fixture?.nativeElement.querySelector('iframe');
    expect(!!iframe).toBeFalse();
  }

  function expectIframeCreationToFail<T>(component: Type<T>): ComponentFixture<T> {
    let fixture: ComponentFixture<T> | undefined;
    expect(() => {
      fixture = TestBed.createComponent(component);
      fixture.detectChanges();
    }).toThrowError(getErrorMessageRegexp());

    ensureNoIframePresent(fixture);
    return fixture!;
  }

  function expectIframeToBeCreated<T>(
    component: Type<T>,
    attrsToCheck: {[key: string]: string},
  ): ComponentFixture<T> {
    let fixture: ComponentFixture<T>;
    expect(() => {
      fixture = TestBed.createComponent(component);
      fixture.detectChanges();
    }).not.toThrow();

    const iframe = fixture!.nativeElement.querySelector('iframe');
    for (const [attrName, attrValue] of Object.entries(attrsToCheck)) {
      expect(iframe[attrName]).toEqual(attrValue);
    }

    return fixture!;
  }

  // *Must* be in sync with the `SECURITY_SENSITIVE_ATTRS` list
  // from the `packages/compiler/src/schema/dom_security_schema.ts`.
  const SECURITY_SENSITIVE_ATTRS = [
    'sandbox',
    'allow',
    'allowFullscreen',
    'referrerPolicy',
    'csp',
    'fetchPriority',
  ];

  const TEST_IFRAME_URL = 'https://angular.io/assets/images/logos/angular/angular.png';

  let oldNgDevMode!: typeof ngDevMode;

  beforeAll(() => {
    oldNgDevMode = ngDevMode;
  });

  afterAll(() => {
    global['ngDevMode'] = oldNgDevMode;
  });

  [true, false].forEach((devModeFlag) => {
    beforeAll(() => {
      global['ngDevMode'] = devModeFlag;

      // TestBed and JIT compilation have some dependencies on the ngDevMode state, so we need to
      // reset TestBed to ensure we get a 'clean' JIT compilation under the new rules.
      TestBed.resetTestingModule();
    });

    describe(`with ngDevMode = ${devModeFlag}`, () => {
      SECURITY_SENSITIVE_ATTRS.forEach((securityAttr: string) => {
        ['src', 'srcdoc'].forEach((srcAttr: string) => {
          it(
            `should work when a security-sensitive attribute is set ` +
              `as a static attribute (checking \`${securityAttr}\` with \`${srcAttr}\`)`,
            () => {
              @Component({
                selector: 'my-comp',
                template: ` <iframe ${srcAttr}="${TEST_IFRAME_URL}" ${securityAttr}=""> </iframe>`,
              })
              class IframeComp {}

              expectIframeToBeCreated(IframeComp, {[srcAttr]: TEST_IFRAME_URL});
            },
          );

          it(
            `should work when a security-sensitive attribute is set ` +
              `as a static attribute (checking \`${securityAttr}\` and ` +
              `making sure it's case-insensitive, with \`${srcAttr}\`)`,
            () => {
              @Component({
                selector: 'my-comp',
                template: ` <iframe
                  ${srcAttr}="${TEST_IFRAME_URL}"
                  ${securityAttr.toUpperCase()}=""
                >
                </iframe>`,
              })
              class IframeComp {}

              expectIframeToBeCreated(IframeComp, {[srcAttr]: TEST_IFRAME_URL});
            },
          );

          it(
            `should error when a security-sensitive attribute is applied ` +
              `using a property binding (checking \`${securityAttr}\`, with \`${srcAttr}\`)`,
            () => {
              @Component({
                selector: 'my-comp',
                template: `<iframe
                  ${srcAttr}="${TEST_IFRAME_URL}"
                  [${securityAttr}]="''"
                ></iframe>`,
              })
              class IframeComp {}

              expectIframeCreationToFail(IframeComp);
            },
          );

          it(
            `should error when a security-sensitive attribute is applied ` +
              `using a property interpolation (checking \`${securityAttr}\`, with \`${srcAttr}\`)`,
            () => {
              @Component({
                selector: 'my-comp',
                template: `<iframe
                  ${srcAttr}="${TEST_IFRAME_URL}"
                  ${securityAttr}="{{ '' }}"
                ></iframe>`,
              })
              class IframeComp {}

              expectIframeCreationToFail(IframeComp);
            },
          );

          it(
            `should error when a security-sensitive attribute is applied ` +
              `using a property binding (checking \`${securityAttr}\`, making ` +
              `sure it's case-insensitive, with \`${srcAttr}\`)`,
            () => {
              @Component({
                selector: 'my-comp',
                template: `
                  <iframe
                    ${srcAttr}="${TEST_IFRAME_URL}"
                    [${securityAttr.toUpperCase()}]="''"
                  ></iframe>
                `,
              })
              class IframeComp {}

              expectIframeCreationToFail(IframeComp);
            },
          );

          it(
            `should error when a security-sensitive attribute is applied ` +
              `using a property binding (checking \`${securityAttr}\` (attr.), with \`${srcAttr}\`)`,
            () => {
              @Component({
                selector: 'my-comp',
                template: `
                  <iframe ${srcAttr}="${TEST_IFRAME_URL}" [attr.${securityAttr}]="''"></iframe>
                `,
              })
              class IframeComp {}

              expectIframeCreationToFail(IframeComp);
            },
          );

          it(
            `should error when a security-sensitive attribute is applied ` +
              `using a property binding (checking \`${securityAttr}\` with [attr.], making ` +
              `sure it's case-insensitive, with \`${srcAttr}\`)`,
            () => {
              @Component({
                selector: 'my-comp',
                template: `
                  <iframe
                    ${srcAttr}="${TEST_IFRAME_URL}"
                    [attr.${securityAttr.toUpperCase()}]="''"
                  ></iframe>
                `,
              })
              class IframeComp {}

              expectIframeCreationToFail(IframeComp);
            },
          );

          it(`should allow changing \`${srcAttr}\` after initial render with \`${securityAttr}\``, () => {
            @Component({
              selector: 'my-comp',
              template: ` <iframe ${securityAttr}="allow-forms" [${srcAttr}]="src"> </iframe> `,
            })
            class IframeComp {
              private sanitizer = inject(DomSanitizer);
              src = this.sanitizeFn(TEST_IFRAME_URL);

              get sanitizeFn() {
                return srcAttr === 'src'
                  ? this.sanitizer.bypassSecurityTrustResourceUrl
                  : this.sanitizer.bypassSecurityTrustHtml;
              }
            }

            const fixture = expectIframeToBeCreated(IframeComp, {[srcAttr]: TEST_IFRAME_URL});
            const component = fixture.componentInstance;

            // Changing `src` or `srcdoc` is allowed.
            const newUrl = 'https://angular.io/about?group=Angular';
            component.src = component.sanitizeFn(newUrl);
            expect(() => fixture.detectChanges()).not.toThrow();
            expect(fixture.nativeElement.querySelector('iframe')[srcAttr]).toEqual(newUrl);
          });
        });
      });

      it('should error when a translated security-sensitive attribute contains bindings', () => {
        @Component({
          selector: 'my-comp',
          template: `
            <iframe
              src="${TEST_IFRAME_URL}"
              i18n-sandbox
              sandbox="allow-forms {{ extraPrivileges }}"
            >
            </iframe>
          `,

          changeDetection: ChangeDetectionStrategy.Eager,
        })
        class IframeComp {
          extraPrivileges = 'allow-scripts allow-same-origin';
        }

        expectIframeCreationToFail(IframeComp);
      });

      it('should work when a directive sets a security-sensitive attribute as a static attribute', () => {
        @Directive({
          selector: '[dir]',
          host: {
            'src': TEST_IFRAME_URL,
            'sandbox': '',
          },
        })
        class IframeDir {}
        @Component({
          imports: [IframeDir],
          selector: 'my-comp',
          template: '<iframe dir></iframe>',
        })
        class IframeComp {}

        expectIframeToBeCreated(IframeComp, {src: TEST_IFRAME_URL});
      });

      it('should work when a directive sets a security-sensitive host attribute on a non-iframe element', () => {
        @Directive({
          selector: '[dir]',
          host: {
            'src': TEST_IFRAME_URL,
            'sandbox': '',
          },
        })
        class Dir {}

        @Component({
          imports: [Dir],
          selector: 'my-comp',
          template: '<img dir>',
        })
        class NonIframeComp {}

        const fixture = TestBed.createComponent(NonIframeComp);
        fixture.detectChanges();

        expect(fixture.nativeElement.firstChild.src).toEqual(TEST_IFRAME_URL);
      });

      it(
        'should work when a security-sensitive attribute on an <iframe> ' +
          'which also has a structural directive (*ngIf)',
        () => {
          @Component({
            imports: [NgIf],
            selector: 'my-comp',
            template: `<iframe *ngIf="visible" src="${TEST_IFRAME_URL}" sandbox=""></iframe>`,
          })
          class IframeComp {
            visible = true;
          }

          expectIframeToBeCreated(IframeComp, {src: TEST_IFRAME_URL});
        },
      );

      it('should work when a security-sensitive attribute is set between `src` and `srcdoc`', () => {
        @Component({
          selector: 'my-comp',
          template: `<iframe src="${TEST_IFRAME_URL}" sandbox srcdoc="Hi!"></iframe>`,
        })
        class IframeComp {}

        expectIframeToBeCreated(IframeComp, {src: TEST_IFRAME_URL});
      });

      it('should work when a directive sets a security-sensitive attribute before setting `src`', () => {
        @Directive({
          selector: '[dir]',
          host: {
            'sandbox': '',
            'src': TEST_IFRAME_URL,
          },
        })
        class IframeDir {}

        @Component({
          imports: [IframeDir],
          selector: 'my-comp',
          template: '<iframe dir></iframe>',
        })
        class IframeComp {}

        expectIframeToBeCreated(IframeComp, {src: TEST_IFRAME_URL});
      });

      it(
        'should work when a directive sets an `src` and ' +
          'there was a security-sensitive attribute set in a template' +
          '(directive attribute after `sandbox`)',
        () => {
          @Directive({
            selector: '[dir]',
            host: {
              'src': TEST_IFRAME_URL,
            },
          })
          class IframeDir {}

          @Component({
            imports: [IframeDir],
            selector: 'my-comp',
            template: '<iframe sandbox dir></iframe>',
          })
          class IframeComp {}

          expectIframeToBeCreated(IframeComp, {src: TEST_IFRAME_URL});
        },
      );

      it(
        'should error when a directive sets a security-sensitive attribute ' +
          "as an attribute binding (checking that it's case-insensitive)",
        () => {
          @Directive({
            selector: '[dir]',
            host: {
              '[attr.SANDBOX]': "''",
            },
          })
          class IframeDir {}

          @Component({
            imports: [IframeDir],
            selector: 'my-comp',
            template: `<IFRAME dir src="${TEST_IFRAME_URL}"></IFRAME>`,
          })
          class IframeComp {}

          expectIframeCreationToFail(IframeComp);
        },
      );

      it(
        'should work when a directive sets an `src` and ' +
          'there was a security-sensitive attribute set in a template' +
          '(directive attribute before `sandbox`)',
        () => {
          @Directive({
            selector: '[dir]',
            host: {
              'src': TEST_IFRAME_URL,
            },
          })
          class IframeDir {}

          @Component({
            imports: [IframeDir],
            selector: 'my-comp',
            template: '<iframe dir sandbox></iframe>',
          })
          class IframeComp {}

          expectIframeToBeCreated(IframeComp, {src: TEST_IFRAME_URL});
        },
      );

      it(
        'should work when a directive sets a security-sensitive attribute and ' +
          'there was an `src` attribute set in a template' +
          '(directive attribute after `src`)',
        () => {
          @Directive({
            selector: '[dir]',
            host: {
              'sandbox': '',
            },
          })
          class IframeDir {}

          @Component({
            imports: [IframeDir],
            selector: 'my-comp',
            template: `<iframe src="${TEST_IFRAME_URL}" dir></iframe>`,
          })
          class IframeComp {}

          expectIframeToBeCreated(IframeComp, {src: TEST_IFRAME_URL});
        },
      );

      it('should work when a security-sensitive attribute is set as a static attribute', () => {
        @Component({
          selector: 'my-comp',
          template: ` <iframe referrerPolicy="no-referrer" src="${TEST_IFRAME_URL}"></iframe> `,
        })
        class IframeComp {}

        expectIframeToBeCreated(IframeComp, {
          src: TEST_IFRAME_URL,
          referrerPolicy: 'no-referrer',
        });
      });

      it(
        'should error when a security-sensitive attribute is set ' +
          'as a property binding and an <iframe> is wrapped into another element',
        () => {
          @Component({
            selector: 'my-comp',
            template: ` <section>
              <iframe src="${TEST_IFRAME_URL}" [referrerPolicy]="'no-referrer'"></iframe>
            </section>`,
          })
          class IframeComp {}

          expectIframeCreationToFail(IframeComp);
        },
      );

      it(
        'should work when a directive sets a security-sensitive attribute and ' +
          'there was an `src` attribute set in a template' +
          '(directive attribute before `src`)',
        () => {
          @Directive({
            selector: '[dir]',
            host: {
              'sandbox': '',
            },
          })
          class IframeDir {}

          @Component({
            imports: [IframeDir],
            selector: 'my-comp',
            template: `<iframe dir src="${TEST_IFRAME_URL}"></iframe>`,
          })
          class IframeComp {}

          expectIframeToBeCreated(IframeComp, {src: TEST_IFRAME_URL});
        },
      );

      it(
        'should work when a directive that sets a security-sensitive attribute goes ' +
          'before the directive that sets an `src` attribute value',
        () => {
          @Directive({
            selector: '[set-src]',
            host: {
              'src': TEST_IFRAME_URL,
            },
          })
          class DirThatSetsSrc {}

          @Directive({
            selector: '[set-sandbox]',
            host: {
              'sandbox': '',
            },
          })
          class DirThatSetsSandbox {}

          @Component({
            imports: [DirThatSetsSandbox, DirThatSetsSrc],
            selector: 'my-comp',
            // Important note: even though the `set-sandbox` goes after the `set-src`,
            // the directive matching order (thus the order of host attributes) is
            // based on the imports order, so the `sandbox` gets set first and the `src` second.
            template: '<iframe set-src set-sandbox></iframe>',
          })
          class IframeComp {}

          expectIframeToBeCreated(IframeComp, {src: TEST_IFRAME_URL});
        },
      );

      it(
        'should work when a directive that sets a security-sensitive attribute has ' +
          'a host directive that sets an `src` attribute value',
        () => {
          @Directive({
            selector: '[set-src-dir]',
            host: {
              'src': TEST_IFRAME_URL,
            },
          })
          class DirThatSetsSrc {}

          @Directive({
            selector: '[dir]',
            hostDirectives: [DirThatSetsSrc],
            host: {
              'sandbox': '',
            },
          })
          class DirThatSetsSandbox {}

          @Component({
            imports: [DirThatSetsSandbox],
            selector: 'my-comp',
            template: '<iframe dir></iframe>',
          })
          class IframeComp {}

          expectIframeToBeCreated(IframeComp, {src: TEST_IFRAME_URL});
        },
      );

      it(
        'should work when a directive that sets an `src` has ' +
          'a host directive that sets a security-sensitive attribute value',
        () => {
          @Directive({
            selector: '[set-sandbox-dir]',
            host: {
              'sandbox': '',
            },
          })
          class DirThatSetsSandbox {}

          @Directive({
            selector: '[dir]',
            hostDirectives: [DirThatSetsSandbox],
            host: {
              'src': TEST_IFRAME_URL,
            },
          })
          class DirThatSetsSrc {}

          @Component({
            imports: [DirThatSetsSrc],
            selector: 'my-comp',
            template: '<iframe dir></iframe>',
          })
          class IframeComp {}

          expectIframeToBeCreated(IframeComp, {src: TEST_IFRAME_URL});
        },
      );

      it(
        'should error when creating a view that contains an <iframe> ' +
          'with security-sensitive attributes set via property bindings',
        () => {
          @Component({
            selector: 'my-comp',
            template: `
              <ng-container #container></ng-container>
              <ng-template #template>
                <iframe src="${TEST_IFRAME_URL}" [sandbox]="''"></iframe>
              </ng-template>
            `,
          })
          class IframeComp {
            @ViewChild('container', {read: ViewContainerRef}) container!: ViewContainerRef;
            @ViewChild('template') template!: TemplateRef<unknown>;

            createEmbeddedView() {
              this.container.createEmbeddedView(this.template);
            }
          }

          const fixture = TestBed.createComponent(IframeComp);
          fixture.detectChanges();

          expect(() => {
            fixture.componentInstance.createEmbeddedView();
            fixture.detectChanges();
          }).toThrowError(getErrorMessageRegexp());

          ensureNoIframePresent(fixture);
        },
      );

      describe('i18n', () => {
        it(
          'should error when a security-sensitive attribute is set as ' +
            'a property binding on an <iframe> inside i18n block',
          () => {
            @Component({
              selector: 'my-comp',
              template: `
                <section i18n>
                  <iframe src="${TEST_IFRAME_URL}" [sandbox]="''"> </iframe>
                </section>
              `,
            })
            class IframeComp {}

            expectIframeCreationToFail(IframeComp);
          },
        );

        it(
          'should error when a security-sensitive attribute is set as ' +
            'a property binding on an <iframe> annotated with i18n attribute',
          () => {
            @Component({
              selector: 'my-comp',
              template: ` <iframe i18n src="${TEST_IFRAME_URL}" [sandbox]="''"> </iframe> `,
            })
            class IframeComp {}

            expectIframeCreationToFail(IframeComp);
          },
        );

        it('should work when a security-sensitive attributes are marked for translation', () => {
          @Component({
            selector: 'my-comp',
            template: ` <iframe src="${TEST_IFRAME_URL}" i18n-sandbox sandbox=""> </iframe> `,
          })
          class IframeComp {}

          expectIframeToBeCreated(IframeComp, {src: TEST_IFRAME_URL});
        });
      });
    });
  });
});

describe('SVG animation processing', () => {
  it('should error when `attributeName` is bound', () => {
    @Component({
      template: '<svg><animate [attr.attributeName]="attr"></animate></svg>',
    })
    class TestCmp {
      attr = 'href';
    }

    expect(() => {
      const fixture = TestBed.createComponent(TestCmp);
      fixture.detectChanges();
    }).toThrowError(
      /NG0910: Angular has detected that the `attributeName` was applied as a binding to the <animate>/,
    );
  });

  it(`should error when a directive sets a 'attributeName' as an attribute binding`, () => {
    @Directive({
      selector: '[dir]',
      host: {
        '[attr.attributeName]': "'href'",
      },
    })
    class animateAttrDir {}

    @Component({
      imports: [animateAttrDir],
      selector: 'my-comp',
      template: '<svg><animate dir></animate></svg>',
    })
    class TestCmp {}

    expect(() => {
      const fixture = TestBed.createComponent(TestCmp);
      fixture.detectChanges();
    }).toThrowError(
      /NG0910: Angular has detected that the `attributeName` was applied as a binding to the <animate>/,
    );
  });
});

describe('innerHTML processing', () => {
  it('should drop risky attributes from elements created with innerHTML', () => {
    @Component({
      template: '<div [innerHTML]="html"></div>',
    })
    class App {
      html = '<div action="abc"></div>';
    }

    const fixture = TestBed.createComponent(App);
    fixture.detectChanges();

    expect(fixture.nativeElement.innerHTML).not.toContain('action');
  });
});

describe('Component host element validation', () => {
  it('should throw an error when dynamically mounting a component onto a script tag', () => {
    @Component({
      selector: 'my-sink',
      template: '',
    })
    class MySink {}

    const scriptHost = document.createElement('script');
    document.head.appendChild(scriptHost);

    try {
      const environmentInjector = TestBed.inject(EnvironmentInjector);
      expect(() => {
        createComponent(MySink, {
          environmentInjector,
          hostElement: scriptHost,
        });
      }).toThrowError(/"<script>" tag is not allowed as a component host element/);
    } finally {
      scriptHost.remove();
    }
  });

  it('should throw an error when dynamically mounting a component onto an SVG script tag', () => {
    @Component({
      selector: 'my-svg-sink',
      template: '',
    })
    class MySvgSink {}

    const svgScriptHost = document.createElementNS('http://www.w3.org/2000/svg', 'script');
    document.head.appendChild(svgScriptHost);

    try {
      const environmentInjector = TestBed.inject(EnvironmentInjector);
      expect(() => {
        createComponent(MySvgSink, {
          environmentInjector,
          hostElement: svgScriptHost,
        });
      }).toThrowError(/"<script>" tag is not allowed as a component host element/);
    } finally {
      svgScriptHost.remove();
    }
  });
});

describe('SVG <script> bindings', () => {
  it(`should remove svg <script> element`, () => {
    @Component({
      template: `<svg><script src="https://bad.com/script.js"></script></svg>`,
      changeDetection: ChangeDetectionStrategy.Eager,
    })
    class TestCmp {}

    const fixture = TestBed.createComponent(TestCmp);
    fixture.detectChanges();
    expect(fixture.nativeElement.querySelector('script')).toBeFalsy();
  });
});
