JSON.pm 4.7 KB

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