JSON.pm 5.0 KB

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