/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "WorkerError.h"

#include <stdio.h>
#include <algorithm>
#include <utility>
#include "MainThreadUtils.h"
#include "WorkerPrivate.h"
#include "WorkerRunnable.h"
#include "WorkerScope.h"
#include "js/ComparisonOperators.h"
#include "js/UniquePtr.h"
#include "js/friend/ErrorMessages.h"
#include "jsapi.h"
#include "mozilla/ArrayAlgorithm.h"
#include "mozilla/ArrayIterator.h"
#include "mozilla/Assertions.h"
#include "mozilla/BasicEvents.h"
#include "mozilla/DOMEventTargetHelper.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/RefPtr.h"
#include "mozilla/Span.h"
#include "mozilla/ThreadSafeWeakPtr.h"
#include "mozilla/Unused.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/ErrorEvent.h"
#include "mozilla/dom/ErrorEventBinding.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/EventBinding.h"
#include "mozilla/dom/EventTarget.h"
#include "mozilla/dom/RemoteWorkerChild.h"
#include "mozilla/dom/RemoteWorkerTypes.h"
#include "mozilla/dom/RootedDictionary.h"
#include "mozilla/dom/ServiceWorkerManager.h"
#include "mozilla/dom/ServiceWorkerUtils.h"
#include "mozilla/dom/SimpleGlobalObject.h"
#include "mozilla/dom/Worker.h"
#include "mozilla/dom/WorkerCommon.h"
#include "mozilla/dom/WorkerDebuggerGlobalScopeBinding.h"
#include "mozilla/dom/WorkerGlobalScopeBinding.h"
#include "mozilla/fallible.h"
#include "nsCOMPtr.h"
#include "nsDebug.h"
#include "nsGlobalWindowOuter.h"
#include "nsIConsoleService.h"
#include "nsIScriptError.h"
#include "nsScriptError.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsWrapperCacheInlines.h"
#include "nscore.h"
#include "xpcpublic.h"

namespace mozilla::dom {

namespace {

class ReportErrorRunnable final : public WorkerDebuggeeRunnable {
  UniquePtr<WorkerErrorReport> mReport;

 public:
  ReportErrorRunnable(WorkerPrivate* aWorkerPrivate,
                      UniquePtr<WorkerErrorReport> aReport)
      : WorkerDebuggeeRunnable(aWorkerPrivate), mReport(std::move(aReport)) {}

 private:
  virtual void PostDispatch(WorkerPrivate* aWorkerPrivate,
                            bool aDispatchResult) override {
    aWorkerPrivate->AssertIsOnWorkerThread();

    // Dispatch may fail if the worker was canceled, no need to report that as
    // an error, so don't call base class PostDispatch.
  }

  virtual bool WorkerRun(JSContext* aCx,
                         WorkerPrivate* aWorkerPrivate) override {
    uint64_t innerWindowId;
    bool fireAtScope = true;

    bool workerIsAcceptingEvents = aWorkerPrivate->IsAcceptingEvents();

    WorkerPrivate* parent = aWorkerPrivate->GetParent();
    if (parent) {
      innerWindowId = 0;
    } else {
      AssertIsOnMainThread();

      // Once a window has frozen its workers, their
      // mMainThreadDebuggeeEventTargets should be paused, and their
      // WorkerDebuggeeRunnables should not be being executed. The same goes for
      // WorkerDebuggeeRunnables sent from child to parent workers, but since a
      // frozen parent worker runs only control runnables anyway, that is taken
      // care of naturally.
      MOZ_ASSERT(!aWorkerPrivate->IsFrozen());

      // Similarly for paused windows; all its workers should have been
      // informed. (Subworkers are unaffected by paused windows.)
      MOZ_ASSERT(!aWorkerPrivate->IsParentWindowPaused());

      if (aWorkerPrivate->IsSharedWorker()) {
        aWorkerPrivate->GetRemoteWorkerController()
            ->ErrorPropagationOnMainThread(mReport.get(),
                                           /* isErrorEvent */ true);
        return true;
      }

      // Service workers do not have a main thread parent global, so normal
      // worker error reporting will crash.  Instead, pass the error to
      // the ServiceWorkerManager to report on any controlled documents.
      if (aWorkerPrivate->IsServiceWorker()) {
        RefPtr<RemoteWorkerChild> actor(
            aWorkerPrivate->GetRemoteWorkerController());

        Unused << NS_WARN_IF(!actor);

        if (actor) {
          actor->ErrorPropagationOnMainThread(nullptr, false);
        }

        return true;
      }

      // The innerWindowId is only required if we are going to ReportError
      // below, which is gated on this condition. The inner window correctness
      // check is only going to succeed when the worker is accepting events.
      if (workerIsAcceptingEvents) {
        aWorkerPrivate->AssertInnerWindowIsCorrect();
        innerWindowId = aWorkerPrivate->WindowID();
      }
    }

    // Don't fire this event if the JS object has been disconnected from the
    // private object.
    if (!workerIsAcceptingEvents) {
      return true;
    }

    WorkerErrorReport::ReportError(aCx, parent, fireAtScope,
                                   aWorkerPrivate->ParentEventTargetRef(),
                                   std::move(mReport), innerWindowId);
    return true;
  }
};

class ReportGenericErrorRunnable final : public WorkerDebuggeeRunnable {
 public:
  static void CreateAndDispatch(WorkerPrivate* aWorkerPrivate) {
    MOZ_ASSERT(aWorkerPrivate);
    aWorkerPrivate->AssertIsOnWorkerThread();

    RefPtr<ReportGenericErrorRunnable> runnable =
        new ReportGenericErrorRunnable(aWorkerPrivate);
    runnable->Dispatch();
  }

 private:
  explicit ReportGenericErrorRunnable(WorkerPrivate* aWorkerPrivate)
      : WorkerDebuggeeRunnable(aWorkerPrivate) {
    aWorkerPrivate->AssertIsOnWorkerThread();
  }

  void PostDispatch(WorkerPrivate* aWorkerPrivate,
                    bool aDispatchResult) override {
    aWorkerPrivate->AssertIsOnWorkerThread();

    // Dispatch may fail if the worker was canceled, no need to report that as
    // an error, so don't call base class PostDispatch.
  }

  bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
    // Once a window has frozen its workers, their
    // mMainThreadDebuggeeEventTargets should be paused, and their
    // WorkerDebuggeeRunnables should not be being executed. The same goes for
    // WorkerDebuggeeRunnables sent from child to parent workers, but since a
    // frozen parent worker runs only control runnables anyway, that is taken
    // care of naturally.
    MOZ_ASSERT(!aWorkerPrivate->IsFrozen());

    // Similarly for paused windows; all its workers should have been informed.
    // (Subworkers are unaffected by paused windows.)
    MOZ_ASSERT(!aWorkerPrivate->IsParentWindowPaused());

    if (aWorkerPrivate->IsSharedWorker()) {
      aWorkerPrivate->GetRemoteWorkerController()->ErrorPropagationOnMainThread(
          nullptr, false);
      return true;
    }

    if (aWorkerPrivate->IsServiceWorker()) {
      RefPtr<RemoteWorkerChild> actor(
          aWorkerPrivate->GetRemoteWorkerController());

      Unused << NS_WARN_IF(!actor);

      if (actor) {
        actor->ErrorPropagationOnMainThread(nullptr, false);
      }

      return true;
    }

    if (!aWorkerPrivate->IsAcceptingEvents()) {
      return true;
    }

    RefPtr<mozilla::dom::EventTarget> parentEventTarget =
        aWorkerPrivate->ParentEventTargetRef();
    RefPtr<Event> event =
        Event::Constructor(parentEventTarget, u"error"_ns, EventInit());
    event->SetTrusted(true);

    parentEventTarget->DispatchEvent(*event);
    return true;
  }
};

}  // namespace

void WorkerErrorBase::AssignErrorBase(JSErrorBase* aReport) {
  CopyUTF8toUTF16(MakeStringSpan(aReport->filename.c_str()), mFilename);
  mLineNumber = aReport->lineno;
  mColumnNumber = aReport->column;
  mErrorNumber = aReport->errorNumber;
}

void WorkerErrorNote::AssignErrorNote(JSErrorNotes::Note* aNote) {
  WorkerErrorBase::AssignErrorBase(aNote);
  xpc::ErrorNote::ErrorNoteToMessageString(aNote, mMessage);
}

WorkerErrorReport::WorkerErrorReport()
    : mIsWarning(false), mExnType(JSEXN_ERR), mMutedError(false) {}

void WorkerErrorReport::AssignErrorReport(JSErrorReport* aReport) {
  WorkerErrorBase::AssignErrorBase(aReport);
  xpc::ErrorReport::ErrorReportToMessageString(aReport, mMessage);

  mLine.Assign(aReport->linebuf(), aReport->linebufLength());
  mIsWarning = aReport->isWarning();
  MOZ_ASSERT(aReport->exnType >= JSEXN_FIRST && aReport->exnType < JSEXN_LIMIT);
  mExnType = JSExnType(aReport->exnType);
  mMutedError = aReport->isMuted;

  if (aReport->notes) {
    if (!mNotes.SetLength(aReport->notes->length(), fallible)) {
      return;
    }

    size_t i = 0;
    for (auto&& note : *aReport->notes) {
      mNotes.ElementAt(i).AssignErrorNote(note.get());
      i++;
    }
  }
}

// aWorkerPrivate is the worker thread we're on (or the main thread, if null)
// aTarget is the worker object that we are going to fire an error at
// (if any).
/* static */
void WorkerErrorReport::ReportError(
    JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aFireAtScope,
    DOMEventTargetHelper* aTarget, UniquePtr<WorkerErrorReport> aReport,
    uint64_t aInnerWindowId, JS::Handle<JS::Value> aException) {
  if (aWorkerPrivate) {
    aWorkerPrivate->AssertIsOnWorkerThread();
  } else {
    AssertIsOnMainThread();
  }

  // We should not fire error events for warnings but instead make sure that
  // they show up in the error console.
  if (!aReport->mIsWarning) {
    // First fire an ErrorEvent at the worker.
    RootedDictionary<ErrorEventInit> init(aCx);

    if (aReport->mMutedError) {
      init.mMessage.AssignLiteral("Script error.");
    } else {
      init.mMessage = aReport->mMessage;
      init.mFilename = aReport->mFilename;
      init.mLineno = aReport->mLineNumber;
      init.mColno = aReport->mColumnNumber;
      init.mError = aException;
    }

    init.mCancelable = true;
    init.mBubbles = false;

    if (aTarget) {
      RefPtr<ErrorEvent> event =
          ErrorEvent::Constructor(aTarget, u"error"_ns, init);
      event->SetTrusted(true);

      bool defaultActionEnabled =
          aTarget->DispatchEvent(*event, CallerType::System, IgnoreErrors());
      if (!defaultActionEnabled) {
        return;
      }
    }

    // Now fire an event at the global object, but don't do that if the error
    // code is too much recursion and this is the same script threw the error.
    // XXXbz the interaction of this with worker errors seems kinda broken.
    // An overrecursion in the debugger or debugger sandbox will get turned
    // into an error event on our parent worker!
    // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks making this
    // better.
    if (aFireAtScope &&
        (aTarget || aReport->mErrorNumber != JSMSG_OVER_RECURSED)) {
      JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx));
      NS_ASSERTION(global, "This should never be null!");

      nsEventStatus status = nsEventStatus_eIgnore;

      if (aWorkerPrivate) {
        RefPtr<WorkerGlobalScope> globalScope;
        UNWRAP_OBJECT(WorkerGlobalScope, &global, globalScope);

        if (!globalScope) {
          WorkerDebuggerGlobalScope* globalScope = nullptr;
          UNWRAP_OBJECT(WorkerDebuggerGlobalScope, &global, globalScope);

          MOZ_ASSERT_IF(globalScope,
                        globalScope->GetWrapperPreserveColor() == global);
          if (globalScope || IsWorkerDebuggerSandbox(global)) {
            aWorkerPrivate->ReportErrorToDebugger(
                aReport->mFilename, aReport->mLineNumber, aReport->mMessage);
            return;
          }

          MOZ_ASSERT(SimpleGlobalObject::SimpleGlobalType(global) ==
                     SimpleGlobalObject::GlobalType::BindingDetail);
          // XXXbz We should really log this to console, but unwinding out of
          // this stuff without ending up firing any events is ... hard.  Just
          // return for now.
          // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks
          // making this better.
          return;
        }

        MOZ_ASSERT(globalScope->GetWrapperPreserveColor() == global);

        RefPtr<ErrorEvent> event =
            ErrorEvent::Constructor(aTarget, u"error"_ns, init);
        event->SetTrusted(true);

        // TODO: Bug 1506441
        if (NS_FAILED(EventDispatcher::DispatchDOMEvent(
                MOZ_KnownLive(ToSupports(globalScope)), nullptr, event, nullptr,
                &status))) {
          NS_WARNING("Failed to dispatch worker thread error event!");
          status = nsEventStatus_eIgnore;
        }
      } else if (nsGlobalWindowInner* win = xpc::WindowOrNull(global)) {
        MOZ_ASSERT(NS_IsMainThread());

        if (!win->HandleScriptError(init, &status)) {
          NS_WARNING("Failed to dispatch main thread error event!");
          status = nsEventStatus_eIgnore;
        }
      }

      // Was preventDefault() called?
      if (status == nsEventStatus_eConsumeNoDefault) {
        return;
      }
    }
  }

  // Now fire a runnable to do the same on the parent's thread if we can.
  if (aWorkerPrivate) {
    RefPtr<ReportErrorRunnable> runnable =
        new ReportErrorRunnable(aWorkerPrivate, std::move(aReport));
    runnable->Dispatch();
    return;
  }

  // Otherwise log an error to the error console.
  WorkerErrorReport::LogErrorToConsole(aCx, *aReport, aInnerWindowId);
}

/* static */
void WorkerErrorReport::LogErrorToConsole(JSContext* aCx,
                                          WorkerErrorReport& aReport,
                                          uint64_t aInnerWindowId) {
  JS::Rooted<JSObject*> stack(aCx, aReport.ReadStack(aCx));
  JS::Rooted<JSObject*> stackGlobal(aCx, JS::CurrentGlobalOrNull(aCx));

  ErrorData errorData(
      aReport.mIsWarning, aReport.mLineNumber, aReport.mColumnNumber,
      aReport.mMessage, aReport.mFilename, aReport.mLine,
      TransformIntoNewArray(aReport.mNotes, [](const WorkerErrorNote& note) {
        return ErrorDataNote(note.mLineNumber, note.mColumnNumber,
                             note.mMessage, note.mFilename);
      }));
  LogErrorToConsole(errorData, aInnerWindowId, stack, stackGlobal);
}

/* static */
void WorkerErrorReport::LogErrorToConsole(const ErrorData& aReport,
                                          uint64_t aInnerWindowId,
                                          JS::Handle<JSObject*> aStack,
                                          JS::Handle<JSObject*> aStackGlobal) {
  AssertIsOnMainThread();

  RefPtr<nsScriptErrorBase> scriptError =
      CreateScriptError(nullptr, JS::NothingHandleValue, aStack, aStackGlobal);

  NS_WARNING_ASSERTION(scriptError, "Failed to create script error!");

  if (scriptError) {
    nsAutoCString category("Web Worker");
    uint32_t flags = aReport.isWarning() ? nsIScriptError::warningFlag
                                         : nsIScriptError::errorFlag;
    if (NS_FAILED(scriptError->nsIScriptError::InitWithWindowID(
            aReport.message(), aReport.filename(), aReport.line(),
            aReport.lineNumber(), aReport.columnNumber(), flags, category,
            aInnerWindowId))) {
      NS_WARNING("Failed to init script error!");
      scriptError = nullptr;
    }

    for (const ErrorDataNote& note : aReport.notes()) {
      nsScriptErrorNote* noteObject = new nsScriptErrorNote();
      noteObject->Init(note.message(), note.filename(), 0, note.lineNumber(),
                       note.columnNumber());
      scriptError->AddNote(noteObject);
    }
  }

  nsCOMPtr<nsIConsoleService> consoleService =
      do_GetService(NS_CONSOLESERVICE_CONTRACTID);
  NS_WARNING_ASSERTION(consoleService, "Failed to get console service!");

  if (consoleService) {
    if (scriptError) {
      if (NS_SUCCEEDED(consoleService->LogMessage(scriptError))) {
        return;
      }
      NS_WARNING("LogMessage failed!");
    } else if (NS_SUCCEEDED(consoleService->LogStringMessage(
                   aReport.message().BeginReading()))) {
      return;
    }
    NS_WARNING("LogStringMessage failed!");
  }

  NS_ConvertUTF16toUTF8 msg(aReport.message());
  NS_ConvertUTF16toUTF8 filename(aReport.filename());

  static const char kErrorString[] = "JS error in Web Worker: %s [%s:%u]";

#ifdef ANDROID
  __android_log_print(ANDROID_LOG_INFO, "Gecko", kErrorString, msg.get(),
                      filename.get(), aReport.lineNumber());
#endif

  fprintf(stderr, kErrorString, msg.get(), filename.get(),
          aReport.lineNumber());
  fflush(stderr);
}

/* static */
void WorkerErrorReport::CreateAndDispatchGenericErrorRunnableToParent(
    WorkerPrivate* aWorkerPrivate) {
  ReportGenericErrorRunnable::CreateAndDispatch(aWorkerPrivate);
}

}  // namespace mozilla::dom
