JSON.pm 4.8 KB

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