spec.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.plugins.spec
  4. ~~~~~~~~~~~~~~~~~~~~~~~
  5. This module provides the core FlaskBB plugin hook definitions
  6. :copyright: (c) 2017 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. from pluggy import HookspecMarker
  10. spec = HookspecMarker('flaskbb')
  11. # Setup Hooks
  12. @spec
  13. def flaskbb_extensions(app):
  14. """Hook for initializing any plugin loaded extensions."""
  15. @spec
  16. def flaskbb_load_translations():
  17. """Hook for registering translation folders."""
  18. @spec
  19. def flaskbb_load_migrations():
  20. """Hook for registering additional migrations."""
  21. @spec
  22. def flaskbb_load_blueprints(app):
  23. """Hook for registering blueprints.
  24. :param app: The application object.
  25. """
  26. @spec
  27. def flaskbb_request_processors(app):
  28. """Hook for registering pre/post request processors.
  29. :param app: The application object.
  30. """
  31. @spec
  32. def flaskbb_errorhandlers(app):
  33. """Hook for registering error handlers.
  34. :param app: The application object.
  35. """
  36. @spec
  37. def flaskbb_jinja_directives(app):
  38. """Hook for registering jinja filters, context processors, etc.
  39. :param app: The application object.
  40. """
  41. @spec
  42. def flaskbb_additional_setup(app, pluggy):
  43. """Hook for any additional setup a plugin wants to do after all other
  44. application setup has finished.
  45. For example, you could apply a WSGI middleware::
  46. @impl
  47. def flaskbb_additional_setup(app):
  48. app.wsgi_app = ProxyFix(app.wsgi_app)
  49. :param app: The application object.
  50. :param pluggy: The pluggy object.
  51. """
  52. @spec
  53. def flaskbb_cli(cli, app):
  54. """Hook for registering CLI commands.
  55. For example::
  56. @impl
  57. def flaskbb_cli(cli):
  58. @cli.command()
  59. def testplugin():
  60. click.echo("Hello Testplugin")
  61. return testplugin
  62. :param app: The application object.
  63. :param cli: The FlaskBBGroup CLI object.
  64. """
  65. @spec
  66. def flaskbb_shell_context():
  67. """Hook for registering shell context handlers
  68. Expected to return a single callable function that returns a dictionary or
  69. iterable of key value pairs.
  70. """
  71. # Event hooks
  72. @spec
  73. def flaskbb_event_post_save_before(post):
  74. """Hook for handling a post before it has been saved.
  75. :param flaskbb.forum.models.Post post: The post which triggered the event.
  76. """
  77. @spec
  78. def flaskbb_event_post_save_after(post, is_new):
  79. """Hook for handling a post after it has been saved.
  80. :param flaskbb.forum.models.Post post: The post which triggered the event.
  81. :param bool is_new: True if the post is new, False if it is an edit.
  82. """
  83. @spec
  84. def flaskbb_event_topic_save_before(topic):
  85. """Hook for handling a topic before it has been saved.
  86. :param flaskbb.forum.models.Topic topic: The topic which triggered the
  87. event.
  88. """
  89. @spec
  90. def flaskbb_event_topic_save_after(topic, is_new):
  91. """Hook for handling a topic after it has been saved.
  92. :param flaskbb.forum.models.Topic topic: The topic which triggered the
  93. event.
  94. :param bool is_new: True if the topic is new, False if it is an edit.
  95. """
  96. @spec
  97. def flaskbb_event_user_registered(username):
  98. """Hook for handling events after a user is registered
  99. :param username: The username of the newly registered user.
  100. """
  101. @spec(firstresult=True)
  102. def flaskbb_authenticate(identifier, secret):
  103. """Hook for authenticating users in FlaskBB.
  104. This hook should return either an instance of
  105. :class:`flaskbb.user.models.User` or None.
  106. If a hook decides that all attempts for authentication
  107. should end, it may raise a
  108. :class:`flaskbb.core.exceptions.StopAuthentication`
  109. and include a reason why authentication was stopped.
  110. Only the first User result will used and the default FlaskBB
  111. authentication is tried last to give others an attempt to
  112. authenticate the user instead.
  113. See also:
  114. :class:`AuthenticationProvider<flaskbb.core.auth.AuthenticationProvider>`
  115. Example of alternative auth::
  116. def ldap_auth(identifier, secret):
  117. "basic ldap example with imaginary ldap library"
  118. user_dn = "uid={},ou=flaskbb,dc=flaskbb,dc=org"
  119. try:
  120. ldap.bind(user_dn, secret)
  121. return User.query.join(
  122. UserLDAP
  123. ).filter(
  124. UserLDAP.dn==user_dn
  125. ).with_entities(User).one()
  126. except:
  127. return None
  128. @impl
  129. def flaskbb_authenticate(identifier, secret):
  130. return ldap_auth(identifier, secret)
  131. Example of ending authentication::
  132. def prevent_login_with_too_many_failed_attempts(identifier):
  133. user = User.query.filter(
  134. db.or_(
  135. User.username == identifier,
  136. User.email == identifier
  137. )
  138. ).first()
  139. if user is not None:
  140. if has_too_many_failed_logins(user):
  141. raise StopAuthentication(_(
  142. "Your account is temporarily locked due to too many login attempts"
  143. ))
  144. @impl(tryfirst=True)
  145. def flaskbb_authenticate(user, identifier):
  146. prevent_login_with_too_many_failed_attempts(identifier)
  147. """
  148. @spec
  149. def flaskbb_post_authenticate(user):
  150. """Hook for handling actions that occur after a user is
  151. authenticated but before setting them as the current user.
  152. This could be used to handle MFA. However, these calls will
  153. be blocking and should be taken into account.
  154. Responses from this hook are not considered at all. If a hook
  155. should need to prevent the user from logging in, it should
  156. register itself as tryfirst and raise a
  157. :class:`flaskbb.core.exceptions.StopAuthentication`
  158. and include why the login was prevented.
  159. See also:
  160. :class:`PostAuthenticationHandler<flaskbb.core.auth.PostAuthenticationHandler>`
  161. Example::
  162. def post_auth(user):
  163. today = utcnow()
  164. if is_anniversary(today, user.date_joined):
  165. flash(_("Happy registerversary!"))
  166. @impl
  167. def flaskbb_post_authenticate(user):
  168. post_auth(user)
  169. """
  170. @spec
  171. def flaskbb_authentication_failed(identifier):
  172. """Hook for handling authentication failure events.
  173. This hook will only be called when no authentication
  174. providers successfully return a user or a
  175. :class:`flaskbb.core.exceptions.StopAuthentication`
  176. is raised during the login process.
  177. See also:
  178. :class:`AuthenticationFailureHandler<flaskbb.core.auth.AuthenticationFailureHandler>`
  179. Example::
  180. def mark_failed_logins(identifier):
  181. user = User.query.filter(
  182. db.or_(
  183. User.username == identifier,
  184. User.email == identifier
  185. )
  186. ).first()
  187. if user is not None:
  188. if user.login_attempts is None:
  189. user.login_attempts = 1
  190. else:
  191. user.login_attempts += 1
  192. user.last_failed_login = utcnow()
  193. """
  194. @spec(firstresult=True)
  195. def flaskbb_reauth_attempt(user, secret):
  196. """Hook for handling reauth in FlaskBB
  197. These hooks receive the currently authenticated user
  198. and the entered secret. Only the first response from
  199. this hook is considered -- similar to the authenticate
  200. hooks. A successful attempt should return True, otherwise
  201. None for an unsuccessful or untried reauth from an
  202. implementation. Reauth will be considered a failure if
  203. no implementation return True.
  204. If a hook decides that a reauthenticate attempt should
  205. cease, it may raise StopAuthentication.
  206. See also:
  207. :class:`ReauthenticateProvider<flaskbb.core.auth.ReauthenticateProvider>`
  208. Example of checking secret or passing to the next implementer::
  209. @impl
  210. def flaskbb_reauth_attempt(user, secret):
  211. if check_password(user.password, secret):
  212. return True
  213. Example of forcefully ending reauth::
  214. @impl
  215. def flaskbb_reauth_attempt(user, secret):
  216. if user.login_attempts > 5:
  217. raise StopAuthentication(_("Too many failed authentication attempts"))
  218. """
  219. @spec
  220. def flaskbb_post_reauth(user):
  221. """Hook called after successfully reauthenticating.
  222. These hooks are called a user has passed the flaskbb_reauth_attempt
  223. hooks but before their reauth is confirmed so a post reauth implementer
  224. may still force a reauth to fail by raising StopAuthentication.
  225. Results from these hooks are not considered.
  226. See also:
  227. :class:`PostReauthenticateHandler<flaskbb.core.auth.PostAuthenticationHandler>`
  228. """
  229. @spec
  230. def flaskbb_reauth_failed(user):
  231. """Hook called if a reauth fails.
  232. These hooks will only be called if no implementation
  233. for flaskbb_reauth_attempt returns a True result or if
  234. an implementation raises StopAuthentication.
  235. If an implementation raises ForceLogout it should register
  236. itself as trylast to give other reauth failed handlers an
  237. opprotunity to run first.
  238. See also:
  239. :class:`ReauthenticateFailureHandler<flaskbb.core.auth.ReauthenticateFailureHandler>`
  240. """
  241. # Form hooks
  242. @spec
  243. def flaskbb_form_new_post(form):
  244. """Hook for modifying the :class:`~flaskbb.forum.forms.ReplyForm`.
  245. For example::
  246. @impl
  247. def flaskbb_form_new_post(form):
  248. form.example = TextField("Example Field", validators=[
  249. DataRequired(message="This field is required"),
  250. Length(min=3, max=50)])
  251. :param form: The :class:`~flaskbb.forum.forms.ReplyForm` class.
  252. """
  253. @spec
  254. def flaskbb_form_new_post_save(form):
  255. """Hook for modifying the :class:`~flaskbb.forum.forms.ReplyForm`.
  256. This hook is called while populating the post object with
  257. the data from the form. The post object will be saved after the hook
  258. call.
  259. :param form: The form object.
  260. :param post: The post object.
  261. """
  262. @spec
  263. def flaskbb_form_new_topic(form):
  264. """Hook for modifying the :class:`~flaskbb.forum.forms.NewTopicForm`
  265. :param form: The :class:`~flaskbb.forum.forms.NewTopicForm` class.
  266. """
  267. @spec
  268. def flaskbb_form_new_topic_save(form, topic):
  269. """Hook for modifying the :class:`~flaskbb.forum.forms.NewTopicForm`.
  270. This hook is called while populating the topic object with
  271. the data from the form. The topic object will be saved after the hook
  272. call.
  273. :param form: The form object.
  274. :param topic: The topic object.
  275. """
  276. # Template Hooks
  277. @spec
  278. def flaskbb_tpl_navigation_before():
  279. """Hook for registering additional navigation items.
  280. in :file:`templates/layout.html`.
  281. """
  282. @spec
  283. def flaskbb_tpl_navigation_after():
  284. """Hook for registering additional navigation items.
  285. in :file:`templates/layout.html`.
  286. """
  287. @spec
  288. def flaskbb_tpl_user_nav_loggedin_before():
  289. """Hook for registering additional user navigational items
  290. which are only shown when a user is logged in.
  291. in :file:`templates/layout.html`.
  292. """
  293. @spec
  294. def flaskbb_tpl_user_nav_loggedin_after():
  295. """Hook for registering additional user navigational items
  296. which are only shown when a user is logged in.
  297. in :file:`templates/layout.html`.
  298. """
  299. @spec
  300. def flaskbb_tpl_form_registration_before(form):
  301. """This hook is emitted in the Registration form **before** the first
  302. input field but after the hidden CSRF token field.
  303. in :file:`templates/auth/register.html`.
  304. :param form: The form object.
  305. """
  306. @spec
  307. def flaskbb_tpl_form_registration_after(form):
  308. """This hook is emitted in the Registration form **after** the last
  309. input field but before the submit field.
  310. in :file:`templates/auth/register.html`.
  311. :param form: The form object.
  312. """
  313. @spec
  314. def flaskbb_tpl_form_user_details_before(form):
  315. """This hook is emitted in the Change User Details form **before** an
  316. input field is rendered.
  317. in :file:`templates/user/change_user_details.html`.
  318. :param form: The form object.
  319. """
  320. @spec
  321. def flaskbb_tpl_form_user_details_after(form):
  322. """This hook is emitted in the Change User Details form **after** the last
  323. input field has been rendered but before the submit field.
  324. in :file:`templates/user/change_user_details.html`.
  325. :param form: The form object.
  326. """
  327. @spec
  328. def flaskbb_tpl_profile_settings_menu():
  329. """This hook is emitted on the user settings page in order to populate the
  330. side bar menu. Implementations of this hook should return a list of tuples
  331. that are view name and display text. The display text will be provided to
  332. the translation service so it is unnecessary to supply translated text.
  333. A plugin can declare a new block by setting the view to None. If this is
  334. done, consider marking the hook implementation with `trylast=True` to
  335. avoid capturing plugins that do not create new blocks.
  336. For example::
  337. @impl(trylast=True)
  338. def flaskbb_tpl_profile_settings_menu():
  339. return [
  340. (None, 'Account Settings'),
  341. ('user.settings', 'General Settings'),
  342. ('user.change_user_details', 'Change User Details'),
  343. ('user.change_email', 'Change E-Mail Address'),
  344. ('user.change_password', 'Change Password')
  345. ]
  346. Hookwrappers for this spec should not be registered as FlaskBB
  347. supplies its own hookwrapper to flatten all the lists into a single list.
  348. in :file:`templates/user/settings_layout.html`
  349. """
  350. @spec
  351. def flaskbb_tpl_admin_settings_menu(user):
  352. """This hook is emitted in the admin panel and used to add additional
  353. navigation links to the admin menu.
  354. Implementations of this hook should return a list of tuples
  355. that are view name, display text and optionally an icon.
  356. The display text will be provided to the translation service so it
  357. is unnecessary to supply translated text.
  358. For example::
  359. @impl(trylast=True)
  360. def flaskbb_tpl_admin_settings_menu():
  361. # only add this item if the user is an admin
  362. if Permission(IsAdmin, identity=current_user):
  363. return [
  364. ("myplugin.foobar", "Foobar", "fa fa-foobar")
  365. ]
  366. Hookwrappers for this spec should not be registered as FlaskBB
  367. supplies its own hookwrapper to flatten all the lists into a single list.
  368. in :file:`templates/management/management_layout.html`
  369. :param user: The current user object.
  370. """
  371. @spec
  372. def flaskbb_tpl_profile_sidebar_stats(user):
  373. """This hook is emitted on the users profile page below the standard
  374. information. For example, it can be used to add additional items
  375. such as a link to the profile.
  376. in :file:`templates/user/profile_layout.html`
  377. :param user: The user object for whom the profile is currently visited.
  378. """
  379. @spec
  380. def flaskbb_tpl_post_author_info_before(user, post):
  381. """This hook is emitted before the information about the
  382. author of a post is displayed (but after the username).
  383. in :file:`templates/forum/topic.html`
  384. :param user: The user object of the post's author.
  385. :param post: The post object.
  386. """
  387. @spec
  388. def flaskbb_tpl_post_author_info_after(user, post):
  389. """This hook is emitted after the information about the
  390. author of a post is displayed (but after the username).
  391. in :file:`templates/forum/topic.html`
  392. :param user: The user object of the post's author.
  393. :param post: The post object.
  394. """
  395. @spec
  396. def flaskbb_tpl_post_content_before(post):
  397. """Hook to do some stuff before the post content is rendered.
  398. in :file:`templates/forum/topic.html`
  399. :param post: The current post object.
  400. """
  401. @spec
  402. def flaskbb_tpl_post_content_after(post):
  403. """Hook to do some stuff after the post content is rendered.
  404. in :file:`templates/forum/topic.html`
  405. :param post: The current post object.
  406. """
  407. @spec
  408. def flaskbb_tpl_post_menu_before(post):
  409. """Hook for inserting a new item at the beginning of the post menu.
  410. in :file:`templates/forum/topic.html`
  411. :param post: The current post object.
  412. """
  413. @spec
  414. def flaskbb_tpl_post_menu_after(post):
  415. """Hook for inserting a new item at the end of the post menu.
  416. in :file:`templates/forum/topic.html`
  417. :param post: The current post object.
  418. """
  419. @spec
  420. def flaskbb_tpl_topic_controls(topic):
  421. """Hook for inserting additional topic moderation controls.
  422. in :file:`templates/forum/topic_controls.html`
  423. :param topic: The current topic object.
  424. """
  425. @spec
  426. def flaskbb_tpl_form_new_post_before(form):
  427. """Hook for inserting a new form field before the first field is
  428. rendered.
  429. For example::
  430. @impl
  431. def flaskbb_tpl_form_new_post_after(form):
  432. return render_template_string(
  433. \"""
  434. <div class="form-group">
  435. <div class="col-md-12 col-sm-12 col-xs-12">
  436. <label>{{ form.example.label.text }}</label>
  437. {{ form.example(class="form-control",
  438. placeholder=form.example.label.text) }}
  439. {%- for error in form.example.errors -%}
  440. <span class="help-block">{{error}}</span>
  441. {%- endfor -%}
  442. </div>
  443. </div>
  444. \"""
  445. in :file:`templates/forum/new_post.html`
  446. :param form: The form object.
  447. """
  448. @spec
  449. def flaskbb_tpl_form_new_post_after(form):
  450. """Hook for inserting a new form field after the last field is
  451. rendered (but before the submit field).
  452. in :file:`templates/forum/new_post.html`
  453. :param form: The form object.
  454. """
  455. @spec
  456. def flaskbb_tpl_form_new_topic_before(form):
  457. """Hook for inserting a new form field before the first field is
  458. rendered (but before the CSRF token).
  459. in :file:`templates/forum/new_topic.html`
  460. :param form: The form object.
  461. """
  462. @spec
  463. def flaskbb_tpl_form_new_topic_after(form):
  464. """Hook for inserting a new form field after the last field is
  465. rendered (but before the submit button).
  466. in :file:`templates/forum/new_topic.html`
  467. :param form: The form object.
  468. """