JSON.pm 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. package Trog::Routes::JSON;
  2. use 5.040;
  3. use re '/aasx';
  4. use warnings FATAL => 'all';
  5. use Clone qw{clone};
  6. use Trog::Utils();
  7. use Trog::Auth();
  8. use Trog::Routes::HTML();
  9. use Trog::Log::Metrics();
  10. # The parameters of a route are OpenAPI 3 parameter objects, which
  11. # TCMS::build_routes puts in the operation that tPSGI validates a request by.
  12. our %routes = (
  13. '/api/catalog' => {
  14. method => 'GET',
  15. callback => \&catalog,
  16. },
  17. '/api/webmanifest' => {
  18. method => 'GET',
  19. callback => \&webmanifest,
  20. },
  21. '/api/version' => {
  22. method => 'GET',
  23. callback => \&version,
  24. },
  25. '/api/auth_change_request/{token}' => {
  26. method => 'GET',
  27. callback => \&process_auth_change_request,
  28. parameters => [
  29. {
  30. name => 'token',
  31. in => 'path',
  32. required => true,
  33. # What Trog::Utils::uuid() makes, ended by \z because perl's $ allows a final newline.
  34. schema => { type => 'string', pattern => '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z' },
  35. },
  36. ],
  37. noindex => 1,
  38. robot_name => '/api/auth_change_request/*',
  39. },
  40. '/api/posts_of_form' => {
  41. method => 'GET',
  42. auth => 1,
  43. parameters => [
  44. # Same shape series.json demands of child_form.
  45. { name => 'form', in => 'query', schema => { type => 'string', pattern => '^[A-Za-z0-9_-]+\.tx\z' } },
  46. ],
  47. callback => \&posts_of_form,
  48. noindex => 1,
  49. },
  50. '/api/requests_per' => {
  51. method => 'GET',
  52. auth => 1,
  53. parameters => [
  54. { name => 'period', in => 'query', schema => { type => 'string', enum => [qw{second minute hour day week month year}] } },
  55. map { { name => $_, in => 'query', schema => { type => 'number' } } } qw{num_periods before code},
  56. ],
  57. callback => \&requests_per,
  58. },
  59. );
  60. # Clone / redact for catalog
  61. my $cloned = clone( \%routes );
  62. foreach my $r ( keys(%$cloned) ) {
  63. delete $cloned->{$r}{callback};
  64. }
  65. # Note to authors, don't forget to update this
  66. my sub _version () {
  67. return '1.0';
  68. }
  69. # Special case of a non data-structure JSON return
  70. sub version {
  71. state $ret = [ 200, [ 'Content-type' => "application/json", ETag => 'version-' . _version() ], [ _version() ] ];
  72. return $ret;
  73. }
  74. my sub _render ( $code, $headers, %data ) {
  75. return Trog::Renderer->render(
  76. code => $code,
  77. data => \%data,
  78. template => 'bogus.tx',
  79. contenttype => 'application/json',
  80. headers => $headers,
  81. # TODO it may be appropriate to cache some of these methods.
  82. nocache => 1,
  83. );
  84. }
  85. sub catalog {
  86. return _render( 200, { ETag => 'catalog-' . _version() }, %$cloned );
  87. }
  88. sub webmanifest {
  89. state $headers = { ETag => 'manifest-' . _version() };
  90. state %manifest = (
  91. "icons" => [ map { +{ "src" => Trog::Themes::url_path( Trog::Themes::themed("img/icon/favicon-$_.png") ), "type" => "image/png", "sizes" => "${_}x$_" } } qw{32 48 167 180 192 512} ],
  92. );
  93. return _render( 200, $headers, %manifest );
  94. }
  95. sub process_auth_change_request ($query) {
  96. my $token = $query->{input}{token};
  97. my $msg = Trog::Auth::process_change_request($token);
  98. return $query->{tpsgi}->forbidden($query) unless $msg;
  99. return _render(
  100. 200, undef,
  101. message => $msg,
  102. result => 'success',
  103. );
  104. }
  105. =head2 posts_of_form
  106. Implements GET /api/posts_of_form. Requires a login.
  107. The id and title of every post of the named type which the caller is allowed to
  108. see. This is what fills a relation field's dropdown in the post editor -- see
  109. www/scripts/post_relations.js and the x-tcms-relations tables in the post type
  110. sidecars.
  111. Only id and title: the caller is populating a picker, and a post's body is
  112. nobody's business here.
  113. =cut
  114. sub posts_of_form ($query) {
  115. my @posts = Trog::Routes::HTML::post_helper( { form => $query->{input}{form}, limit => 0 }, [], [ Trog::Routes::HTML::viewer_acls($query) ] );
  116. return _render(
  117. 200, undef,
  118. posts => [ map { { id => $_->{id}, title => $_->{title} } } @posts ],
  119. );
  120. }
  121. sub requests_per ($query) {
  122. my $in = $query->{input};
  123. my $code = Trog::Utils::coerce_array( $in->{code} );
  124. return _render(
  125. 200, undef,
  126. %{ Trog::Log::Metrics::requests_per( $in->{period}, $in->{num_periods}, $in->{before}, @$code ) }
  127. );
  128. }
  129. 1;