defmodule Imagine.CmsTemplates.CmsTemplate do @moduledoc """ CMS Template (versioned) """ use Ecto.Schema import Ecto.Changeset alias Imagine.CmsPages.CmsPage schema "cms_templates" do has_many :versions, Imagine.CmsTemplates.CmsTemplateVersion field :version, :integer field :name, :string field :description, :string # new fields field :content_eex, :string field :options_json, :string # legacy fields (deprecated, but retained so that 5 and 6 can operate side-by-side on the same db) field :content, :string field :options_yaml, :string timestamps(inserted_at: :created_on, updated_at: :updated_on) end @doc false def changeset(cms_template, attrs) do content = attrs["content_eex"] || attrs[:content_eex] || cms_template.content_eex cms_template |> cast(attrs, [:name, :description, :options_json]) |> put_change(:version, (cms_template.version || 0) + 1) |> put_change(:content_eex, content) |> validate_required([:name, :version]) end def test_render(%Ecto.Changeset{} = changeset) do content = get_field(changeset, :content_eex) cms_page = %CmsPage{id: 1, objects: [], versions: []} test_conn = %Plug.Conn{} try do {:ok, render(:view, content, cms_page, test_conn)} rescue e in CompileError -> {:error, e} e in SyntaxError -> {:error, e} e in TokenMissingError -> {:error, e} e in EEx.SyntaxError -> {:error, Map.put(e, :description, e.message)} e in UndefinedFunctionError -> {:error, Map.merge(e, %{description: UndefinedFunctionError.message(e)})} e in Protocol.UndefinedError -> {:error, Map.merge(e, %{description: Protocol.UndefinedError.message(e)})} end end @render_helpers "use Phoenix.HTML; alias Imagine.{CmsPages, CmsPages.CmsPage}; import Kernel, only: [sigil_r: 2, sigil_s: 2, sigil_S: 2, if: 2, ==: 2, !=: 2, |>: 2, ||: 2, &&: 2];" def render(:view, content, cms_page, %Plug.Conn{} = conn) do header = "<% import Imagine.CmsTemplates.RenderViewHelpers; #{@render_helpers} %>" EEx.eval_string( header <> (content || ""), [assigns: [conn: Plug.Conn.assign(conn, :cms_page, cms_page), cms_page: cms_page]], engine: Phoenix.HTML.Engine ) end # someday take a closer look at this: # https://elixirforum.com/t/using-code-eval-string-to-call-other-functions-in-the-module/12866/7 def render(:edit, content, cms_page, %Plug.Conn{} = conn) do # keep this to one line (without a newline at the end) so that line numbers in error messages make sense header = "<% import Imagine.CmsTemplates.RenderEditHelpers; #{@render_helpers} %>" EEx.eval_string( header <> (content || ""), [assigns: [conn: Plug.Conn.assign(conn, :cms_page, cms_page), cms_page: cms_page]], engine: Phoenix.HTML.Engine ) end # *very* rudimentary security measure to sanitize the worst (expected!) attack vectors def sanitize_content(nil), do: nil def sanitize_content(content) do # list all modules in this application {:ok, modules} = :application.get_key(:imagine_cms, :modules) # add on a few more dangerous ones modules = (modules ++ [ Agent, Application, Behaviour, Code, Config, Dict, DynamicSupervisor, EEx, Exunit, File, GenEvent, GenServer, HashDict, HashSet, IEx, IO, Macro, Mix, Node, Path, Port, Process, Registry, Set, Supervisor, System, Task, Ecto, Phoenix, Plug, :file ]) |> Enum.map(&to_string/1) |> Enum.map(fn s -> String.replace(s, ~r/^Elixir\./, "") end) |> Enum.sort(fn a, b -> String.length(a) > String.length(b) end) content |> escape_modules(modules) |> escape_import_and_alias() end # deletes banned modules from template code defp escape_modules(content, []), do: content defp escape_modules(content, [module | modules]) do escape_modules(String.replace(content, ~r/(<%.*?)#{module}\.(.*?%>)/ms, "\\1\\2"), modules) end # deletes import and alias keywords, e.g. "import EEx", "alias Imagine.Repo" defp escape_import_and_alias(content) do String.replace(content, ~r/(<%.*?)(import|alias)([\(\s].*?%>)/ms, "\\1\\3") end end