# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals
import frappe, os, json

from frappe.website.doctype.website_settings.website_settings import get_website_settings
from frappe.website.router import get_page_context
from frappe.model.document import Document

def get_context(path, args=None):
	if args and args.source:
		context = args
	else:
		context = get_page_context(path)
		if args:
			context.update(args)

	if hasattr(frappe.local, 'request'):
		# for <body data-path=""> (remove leading slash)
		# path could be overriden in render.resolve_from_map
		context["path"] = frappe.local.request.path.strip('/ ')
	else:
		context["path"] = path

	context.canonical = frappe.utils.get_url(frappe.utils.escape_html(context.path))
	context.route = context.path
	context = build_context(context)

	# set using frappe.respond_as_web_page
	if hasattr(frappe.local, 'response') and frappe.local.response.get('context'):
		context.update(frappe.local.response.context)

	# to be able to inspect the context dict
	# Use the macro "inspect" from macros.html
	context._context_dict = context

	context.developer_mode = frappe.conf.developer_mode

	return context

def update_controller_context(context, controller):
	module = frappe.get_module(controller)

	if module:
		# get config fields
		for prop in ("base_template_path", "template", "no_cache", "sitemap",
			"condition_field"):
			if hasattr(module, prop):
				context[prop] = getattr(module, prop)

		if hasattr(module, "get_context"):
			import inspect
			try:
				if inspect.getargspec(module.get_context).args:
					ret = module.get_context(context)
				else:
					ret = module.get_context()
				if ret:
					context.update(ret)
			except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect):
				raise
			except:
				if not frappe.flags.in_migrate:
					frappe.errprint(frappe.utils.get_traceback())

		if hasattr(module, "get_children"):
			context.children = module.get_children(context)


def build_context(context):
	"""get_context method of doc or module is supposed to render
		content templates and push it into context"""
	context = frappe._dict(context)

	if not "url_prefix" in context:
		context.url_prefix = ""

	if context.url_prefix and context.url_prefix[-1]!='/':
		context.url_prefix += '/'

	# for backward compatibility
	context.docs_base_url = '/docs'

	context.update(get_website_settings(context))
	context.update(frappe.local.conf.get("website_context") or {})

	# provide doc
	if context.doc:
		context.update(context.doc.as_dict())
		context.update(context.doc.get_website_properties())

		if not context.template:
			context.template = context.doc.meta.get_web_template()

		if hasattr(context.doc, "get_context"):
			ret = context.doc.get_context(context)

			if ret:
				context.update(ret)

		for prop in ("no_cache", "sitemap"):
			if not prop in context:
				context[prop] = getattr(context.doc, prop, False)

	elif context.controller:
		# controller based context
		update_controller_context(context, context.controller)

		# controller context extensions
		context_controller_hooks = frappe.get_hooks("extend_website_page_controller_context") or {}
		for controller, extension in context_controller_hooks.items():
			if isinstance(extension, list):
				for ext in extension:
					if controller == context.controller:
						update_controller_context(context, ext)
			else:
				update_controller_context(context, extension)

	add_metatags(context)
	add_sidebar_and_breadcrumbs(context)

	# determine templates to be used
	if not context.base_template_path:
		app_base = frappe.get_hooks("base_template")
		context.base_template_path = app_base[-1] if app_base else "templates/base.html"

	if context.title_prefix and context.title and not context.title.startswith(context.title_prefix):
		context.title = '{0} - {1}'.format(context.title_prefix, context.title)

	# apply context from hooks
	update_website_context = frappe.get_hooks('update_website_context')
	for method in update_website_context:
		values = frappe.get_attr(method)(context)
		if values:
			context.update(values)

	return context

def load_sidebar(context, sidebar_json_path):
	with open(sidebar_json_path, 'r') as sidebarfile:
		try:
			sidebar_json = sidebarfile.read()
			context.sidebar_items = json.loads(sidebar_json)
			context.show_sidebar = 1
		except json.decoder.JSONDecodeError:
			frappe.throw('Invalid Sidebar JSON at ' + sidebar_json_path)

def get_sidebar_json_path(path, look_for=False):
	'''
		Get _sidebar.json path from directory path

		:param path: path of the current diretory
		:param look_for: if True, look for _sidebar.json going upwards from given path

		:return: _sidebar.json path
	'''
	if os.path.split(path)[1] == 'www' or path == '/' or not path:
		return ''

	sidebar_json_path = os.path.join(path, '_sidebar.json')
	if os.path.exists(sidebar_json_path):
		return sidebar_json_path
	else:
		if look_for:
			return get_sidebar_json_path(os.path.split(path)[0], look_for)
		else:
			return ''

def add_sidebar_and_breadcrumbs(context):
	'''Add sidebar and breadcrumbs to context'''
	from frappe.website.router import get_page_info_from_template
	if context.show_sidebar:
		context.no_cache = 1
		add_sidebar_data(context)
	else:
		if context.basepath:
			hooks = frappe.get_hooks('look_for_sidebar_json')
			look_for_sidebar_json = hooks[0] if hooks else 0
			sidebar_json_path = get_sidebar_json_path(
				context.basepath,
				look_for_sidebar_json
			)
			if sidebar_json_path:
				load_sidebar(context, sidebar_json_path)

	if context.add_breadcrumbs and not context.parents:
		if context.basepath:
			parent_path = os.path.dirname(context.path).rstrip('/')
			page_info = get_page_info_from_template(parent_path)
			if page_info:
				context.parents = [dict(route=parent_path, title=page_info.title)]

def add_sidebar_data(context):
	from frappe.utils.user import get_fullname_and_avatar
	import frappe.www.list

	if context.show_sidebar and context.website_sidebar:
		context.sidebar_items = frappe.get_all('Website Sidebar Item',
			filters=dict(parent=context.website_sidebar), fields=['title', 'route', '`group`'],
			order_by='idx asc')

	if not context.sidebar_items:
		sidebar_items = frappe.cache().hget('portal_menu_items', frappe.session.user)
		if sidebar_items == None:
			sidebar_items = []
			roles = frappe.get_roles()
			portal_settings = frappe.get_doc('Portal Settings', 'Portal Settings')

			def add_items(sidebar_items, items):
				for d in items:
					if d.get('enabled') and ((not d.get('role')) or d.get('role') in roles):
						sidebar_items.append(d.as_dict() if isinstance(d, Document) else d)

			if not portal_settings.hide_standard_menu:
				add_items(sidebar_items, portal_settings.get('menu'))

			if portal_settings.custom_menu:
				add_items(sidebar_items, portal_settings.get('custom_menu'))

			items_via_hooks = frappe.get_hooks('portal_menu_items')
			if items_via_hooks:
				for i in items_via_hooks: i['enabled'] = 1
				add_items(sidebar_items, items_via_hooks)

			frappe.cache().hset('portal_menu_items', frappe.session.user, sidebar_items)

		context.sidebar_items = sidebar_items

	info = get_fullname_and_avatar(frappe.session.user)
	context["fullname"] = info.fullname
	context["user_image"] = info.avatar
	context["user"] = info.name


def add_metatags(context):
	tags = frappe._dict(context.get("metatags") or {})

	if "og:type" not in tags:
		tags["og:type"] = "article"

	if "title" not in tags and context.title:
		tags["title"] = context.title

	title = tags.get("name") or tags.get("title")
	if title:
		tags["og:title"] = tags["twitter:title"] = title
		tags["twitter:card"] = "summary"

	if "description" not in tags and context.description:
		tags["description"] = context.description

	description = tags.get("description")
	if description:
		tags["og:description"] = tags["twitter:description"] = description

	if "image" not in tags and context.image:
		tags["image"] = context.image

	image = tags.get("image")
	if image:
		tags["og:image"] = tags["twitter:image"] = tags["image"] = frappe.utils.get_url(image)
		tags['twitter:card'] = "summary_large_image"

	if "author" not in tags and context.author:
		tags["author"] = context.author

	tags["og:url"] = tags["url"] = frappe.utils.get_url(context.path)

	if "published_on" not in tags and context.published_on:
		tags["published_on"] = context.published_on

	if "published_on" in tags:
		tags["datePublished"] = tags["published_on"]
		del tags["published_on"]

	tags["language"] = frappe.local.lang or "en"

	# Get meta tags from Website Route meta
	# they can override the defaults set above
	route = context.path
	if route == '':
		# homepage
		route = frappe.db.get_single_value('Website Settings', 'home_page')

	route_exists = (route
		and not route.endswith(('.js', '.css'))
		and frappe.db.exists('Website Route Meta', route))

	if route_exists:
		website_route_meta = frappe.get_doc('Website Route Meta', route)
		for meta_tag in website_route_meta.meta_tags:
			d = meta_tag.get_meta_dict()
			tags.update(d)

	# update tags in context
	context.metatags = tags

frappe/website/context.py
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals
import frappe, os, json

from frappe.website.doctype.website_settings.website_settings import get_website_settings
from frappe.website.router import get_page_context
from frappe.model.document import Document

def get_context(path, args=None):
	if args and args.source:
		context = args
	else:
		context = get_page_context(path)
		if args:
			context.update(args)

	if hasattr(frappe.local, 'request'):
		# for <body data-path=""> (remove leading slash)
		# path could be overriden in render.resolve_from_map
		context["path"] = frappe.local.request.path.strip('/ ')
	else:
		context["path"] = path

	context.canonical = frappe.utils.get_url(frappe.utils.escape_html(context.path))
	context.route = context.path
	context = build_context(context)

	# set using frappe.respond_as_web_page
	if hasattr(frappe.local, 'response') and frappe.local.response.get('context'):
		context.update(frappe.local.response.context)

	# to be able to inspect the context dict
	# Use the macro "inspect" from macros.html
	context._context_dict = context

	context.developer_mode = frappe.conf.developer_mode

	return context

def update_controller_context(context, controller):
	module = frappe.get_module(controller)

	if module:
		# get config fields
		for prop in ("base_template_path", "template", "no_cache", "sitemap",
			"condition_field"):
			if hasattr(module, prop):
				context[prop] = getattr(module, prop)

		if hasattr(module, "get_context"):
			import inspect
			try:
				if inspect.getargspec(module.get_context).args:
					ret = module.get_context(context)
				else:
					ret = module.get_context()
				if ret:
					context.update(ret)
			except (frappe.PermissionError, frappe.PageDoesNotExistError, frappe.Redirect):
				raise
			except:
				if not frappe.flags.in_migrate:
					frappe.errprint(frappe.utils.get_traceback())

		if hasattr(module, "get_children"):
			context.children = module.get_children(context)


def build_context(context):
	"""get_context method of doc or module is supposed to render
		content templates and push it into context"""
	context = frappe._dict(context)

	if not "url_prefix" in context:
		context.url_prefix = ""

	if context.url_prefix and context.url_prefix[-1]!='/':
		context.url_prefix += '/'

	# for backward compatibility
	context.docs_base_url = '/docs'

	context.update(get_website_settings(context))
	context.update(frappe.local.conf.get("website_context") or {})

	# provide doc
	if context.doc:
		context.update(context.doc.as_dict())
		context.update(context.doc.get_website_properties())

		if not context.template:
			context.template = context.doc.meta.get_web_template()

		if hasattr(context.doc, "get_context"):
			ret = context.doc.get_context(context)

			if ret:
				context.update(ret)

		for prop in ("no_cache", "sitemap"):
			if not prop in context:
				context[prop] = getattr(context.doc, prop, False)

	elif context.controller:
		# controller based context
		update_controller_context(context, context.controller)

		# controller context extensions
		context_controller_hooks = frappe.get_hooks("extend_website_page_controller_context") or {}
		for controller, extension in context_controller_hooks.items():
			if isinstance(extension, list):
				for ext in extension:
					if controller == context.controller:
						update_controller_context(context, ext)
			else:
				update_controller_context(context, extension)

	add_metatags(context)
	add_sidebar_and_breadcrumbs(context)

	# determine templates to be used
	if not context.base_template_path:
		app_base = frappe.get_hooks("base_template")
		context.base_template_path = app_base[-1] if app_base else "templates/base.html"

	if context.title_prefix and context.title and not context.title.startswith(context.title_prefix):
		context.title = '{0} - {1}'.format(context.title_prefix, context.title)

	# apply context from hooks
	update_website_context = frappe.get_hooks('update_website_context')
	for method in update_website_context:
		values = frappe.get_attr(method)(context)
		if values:
			context.update(values)

	return context

def load_sidebar(context, sidebar_json_path):
	with open(sidebar_json_path, 'r') as sidebarfile:
		try:
			sidebar_json = sidebarfile.read()
			context.sidebar_items = json.loads(sidebar_json)
			context.show_sidebar = 1
		except json.decoder.JSONDecodeError:
			frappe.throw('Invalid Sidebar JSON at ' + sidebar_json_path)

def get_sidebar_json_path(path, look_for=False):
	'''
		Get _sidebar.json path from directory path

		:param path: path of the current diretory
		:param look_for: if True, look for _sidebar.json going upwards from given path

		:return: _sidebar.json path
	'''
	if os.path.split(path)[1] == 'www' or path == '/' or not path:
		return ''

	sidebar_json_path = os.path.join(path, '_sidebar.json')
	if os.path.exists(sidebar_json_path):
		return sidebar_json_path
	else:
		if look_for:
			return get_sidebar_json_path(os.path.split(path)[0], look_for)
		else:
			return ''

def add_sidebar_and_breadcrumbs(context):
	'''Add sidebar and breadcrumbs to context'''
	from frappe.website.router import get_page_info_from_template
	if context.show_sidebar:
		context.no_cache = 1
		add_sidebar_data(context)
	else:
		if context.basepath:
			hooks = frappe.get_hooks('look_for_sidebar_json')
			look_for_sidebar_json = hooks[0] if hooks else 0
			sidebar_json_path = get_sidebar_json_path(
				context.basepath,
				look_for_sidebar_json
			)
			if sidebar_json_path:
				load_sidebar(context, sidebar_json_path)

	if context.add_breadcrumbs and not context.parents:
		if context.basepath:
			parent_path = os.path.dirname(context.path).rstrip('/')
			page_info = get_page_info_from_template(parent_path)
			if page_info:
				context.parents = [dict(route=parent_path, title=page_info.title)]

def add_sidebar_data(context):
	from frappe.utils.user import get_fullname_and_avatar
	import frappe.www.list

	if context.show_sidebar and context.website_sidebar:
		context.sidebar_items = frappe.get_all('Website Sidebar Item',
			filters=dict(parent=context.website_sidebar), fields=['title', 'route', '`group`'],
			order_by='idx asc')

	if not context.sidebar_items:
		sidebar_items = frappe.cache().hget('portal_menu_items', frappe.session.user)
		if sidebar_items == None:
			sidebar_items = []
			roles = frappe.get_roles()
			portal_settings = frappe.get_doc('Portal Settings', 'Portal Settings')

			def add_items(sidebar_items, items):
				for d in items:
					if d.get('enabled') and ((not d.get('role')) or d.get('role') in roles):
						sidebar_items.append(d.as_dict() if isinstance(d, Document) else d)

			if not portal_settings.hide_standard_menu:
				add_items(sidebar_items, portal_settings.get('menu'))

			if portal_settings.custom_menu:
				add_items(sidebar_items, portal_settings.get('custom_menu'))

			items_via_hooks = frappe.get_hooks('portal_menu_items')
			if items_via_hooks:
				for i in items_via_hooks: i['enabled'] = 1
				add_items(sidebar_items, items_via_hooks)

			frappe.cache().hset('portal_menu_items', frappe.session.user, sidebar_items)

		context.sidebar_items = sidebar_items

	info = get_fullname_and_avatar(frappe.session.user)
	context["fullname"] = info.fullname
	context["user_image"] = info.avatar
	context["user"] = info.name


def add_metatags(context):
	tags = frappe._dict(context.get("metatags") or {})

	if "og:type" not in tags:
		tags["og:type"] = "article"

	if "title" not in tags and context.title:
		tags["title"] = context.title

	title = tags.get("name") or tags.get("title")
	if title:
		tags["og:title"] = tags["twitter:title"] = title
		tags["twitter:card"] = "summary"

	if "description" not in tags and context.description:
		tags["description"] = context.description

	description = tags.get("description")
	if description:
		tags["og:description"] = tags["twitter:description"] = description

	if "image" not in tags and context.image:
		tags["image"] = context.image

	image = tags.get("image")
	if image:
		tags["og:image"] = tags["twitter:image"] = tags["image"] = frappe.utils.get_url(image)
		tags['twitter:card'] = "summary_large_image"

	if "author" not in tags and context.author:
		tags["author"] = context.author

	tags["og:url"] = tags["url"] = frappe.utils.get_url(context.path)

	if "published_on" not in tags and context.published_on:
		tags["published_on"] = context.published_on

	if "published_on" in tags:
		tags["datePublished"] = tags["published_on"]
		del tags["published_on"]

	tags["language"] = frappe.local.lang or "en"

	# Get meta tags from Website Route meta
	# they can override the defaults set above
	route = context.path
	if route == '':
		# homepage
		route = frappe.db.get_single_value('Website Settings', 'home_page')

	route_exists = (route
		and not route.endswith(('.js', '.css'))
		and frappe.db.exists('Website Route Meta', route))

	if route_exists:
		website_route_meta = frappe.get_doc('Website Route Meta', route)
		for meta_tag in website_route_meta.meta_tags:
			d = meta_tag.get_meta_dict()
			tags.update(d)

	# update tags in context
	context.metatags = tags

frappe/website/context.py
diff --git a/frappe/website/context.py b/frappe/website/context.py
index 94bcb15e2e92..f6a4a6401926 100644
--- a/frappe/website/context.py
+++ b/frappe/website/context.py
@@ -58,7 +58,7 @@ def update_controller_context(context, controller):
 					ret = module.get_context()
 				if ret:
 					context.update(ret)
-			except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect):
+			except (frappe.PermissionError, frappe.PageDoesNotExistError, frappe.Redirect):
 				raise
 			except:
 				if not frappe.flags.in_migrate:

It is not possible to return 404 from portal pages
<!--
Welcome to the Frappe Framework issue tracker! Before creating an issue, please heed the following:

1. This tracker should only be used to report bugs and request features / enhancements to Frappe
    - For questions and general support, use https://stackoverflow.com/questions/tagged/frappe
    - For documentation issues, refer to https://frappeframework.com/docs/user/en or the developer cheetsheet https://github.com/frappe/frappe/wiki/Developer-Cheatsheet
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
   the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for
   maintainers to reproduce, the faster it'll be fixed.
4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
-->

## Description of the issue

There is no way to return 404 from the portal pages. 

When enquired about it in the _Frappe Framework Public_ Telegram group, @netchampfaris suggested to `raise frappe.PageDoesNotExistError`.  But that didn't work either. 

It looks like the [render.py][1] is handling the `PageDoesNotExistError`, but the [context.py][2] is handling only DoesNotExistError. Because of this, raising none of those exceptions work.

[1]: https://github.com/frappe/frappe/blob/develop/frappe/website/render.py#L50 
[2]: https://github.com/frappe/frappe/blob/develop/frappe/website/context.py#L61

## Context information (for bug reports)

**Output of `bench version`**
```
(paste here)
```

## Steps to reproduce the issue

1. Create a portal page with `raise frappe.PageDoesNotExist()` in the `get_context` function
2. Open that portal page in the browser
3. You'll see a portal page  (or 500 if a variable that is defined after the raise statement is used) instead of 404

### Observed result

a portal page  with 200 OK (or 500 if a variable that is defined after the raise statement is used)

### Expected result

404 Not Found

### Stacktrace / full error message

Not applicable.

## Additional information

OS version / distribution: Mac OS X 11.2.3

```
$ bench version
community 0.0.1
frappe 13.x.x-develop
```
