blob: bbe0998d8669ac46ab9b78b5df8f9d2bb00971c2 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""Classes that help display pagination widgets for result sets."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import base64
12import logging
13import hmac
14
15import ezt
16from google.protobuf import message
17
18import settings
19from framework import exceptions
20from framework import framework_helpers
21from services import secrets_svc
22from proto import secrets_pb2
23
24
25def GeneratePageToken(request_contents, start):
26 # type: (secrets_pb2.ListRequestContents, int) -> str
27 """Encrypts a List requests's contents and generates a next page token.
28
29 Args:
30 request_contents: ListRequestContents object that holds data given by the
31 request.
32 start: int start index that should be used for the subsequent request.
33
34 Returns:
35 String next_page_token that is a serialized PageTokenContents object.
36 """
37 digester = hmac.new(secrets_svc.GetPaginationKey())
38 digester.update(request_contents.SerializeToString())
39 token_contents = secrets_pb2.PageTokenContents(
40 start=start,
41 encrypted_list_request_contents=digester.digest())
42 serialized_token = token_contents.SerializeToString()
43 # Page tokens must be URL-safe strings (see aip.dev/158)
44 # and proto string fields must be utf-8 strings while
45 # `SerializeToString()` returns binary bytes contained in a str type.
46 # So we must encode with web-safe base64 format.
47 return base64.b64encode(serialized_token)
48
49
50def ValidateAndParsePageToken(token, request_contents):
51 # type: (str, secrets_pb2.ListRequestContents) -> int
52 """Returns the start index of the page if the token is valid.
53
54 Args:
55 token: String token given in a ListFoo API request.
56 request_contents: ListRequestContents object that holds data given by the
57 request.
58
59 Returns:
60 The start index that should be used when getting the requested page.
61
62 Raises:
63 PageTokenException: if the token is invalid or incorrect for the given
64 request_contents.
65 """
66 token_contents = secrets_pb2.PageTokenContents()
67 try:
68 decoded_serialized_token = base64.b64decode(token)
69 token_contents.ParseFromString(decoded_serialized_token)
70 except (message.DecodeError, TypeError):
71 raise exceptions.PageTokenException('Invalid page token.')
72
73 start = token_contents.start
74 expected_token = GeneratePageToken(request_contents, start)
75 if hmac.compare_digest(token, expected_token):
76 return start
77 raise exceptions.PageTokenException(
78 'Request parameters must match those from the previous request.')
79
80
81# If extracting items_per_page and start values from a MonorailRequest object,
82# keep in mind that mr.num and mr.GetPositiveIntParam may return different
83# values. mr.num is the result of calling mr.GetPositiveIntParam with a default
84# value.
85class VirtualPagination(object):
86 """Class to calc Prev and Next pagination links based on result counts."""
87
88 def __init__(self, total_count, items_per_page, start, list_page_url=None,
89 count_up=True, start_param_name='start', num_param_name='num',
90 max_num=None, url_params=None, project_name=None):
91 """Given 'num' and 'start' params, determine Prev and Next links.
92
93 Args:
94 total_count: total number of artifacts that satisfy the query.
95 items_per_page: number of items to display on each page, e.g., 25.
96 start: the start index of the pagination page.
97 list_page_url: URL of the web application page that is displaying
98 the list of artifacts. Used to build the Prev and Next URLs.
99 If None, no URLs will be built.
100 count_up: if False, count down from total_count.
101 start_param_name: query string parameter name for the start value
102 of the pagination page.
103 num_param: query string parameter name for the number of items
104 to show on a pagination page.
105 max_num: optional limit on the value of the num param. If not given,
106 settings.max_artifact_search_results_per_page is used.
107 url_params: list of (param_name, param_value) we want to keep
108 in any new urls.
109 project_name: the name of the project we are operating in.
110 """
111 self.total_count = total_count
112 self.prev_url = ''
113 self.reload_url = ''
114 self.next_url = ''
115
116 if max_num is None:
117 max_num = settings.max_artifact_search_results_per_page
118
119 self.num = items_per_page
120 self.num = min(self.num, max_num)
121
122 if count_up:
123 self.start = start or 0
124 self.last = min(self.total_count, self.start + self.num)
125 prev_start = max(0, self.start - self.num)
126 next_start = self.start + self.num
127 else:
128 self.start = start or self.total_count
129 self.last = max(0, self.start - self.num)
130 prev_start = min(self.total_count, self.start + self.num)
131 next_start = self.start - self.num
132
133 if list_page_url:
134 if project_name:
135 list_servlet_rel_url = '/p/%s%s' % (
136 project_name, list_page_url)
137 else:
138 list_servlet_rel_url = list_page_url
139
140 self.reload_url = framework_helpers.FormatURL(
141 url_params, list_servlet_rel_url,
142 **{start_param_name: self.start, num_param_name: self.num})
143
144 if prev_start != self.start:
145 self.prev_url = framework_helpers.FormatURL(
146 url_params, list_servlet_rel_url,
147 **{start_param_name: prev_start, num_param_name: self.num})
148 if ((count_up and next_start < self.total_count) or
149 (not count_up and next_start >= 1)):
150 self.next_url = framework_helpers.FormatURL(
151 url_params, list_servlet_rel_url,
152 **{start_param_name: next_start, num_param_name: self.num})
153
154 self.visible = ezt.boolean(self.last != self.start)
155
156 # Adjust indices to one-based values for display to users.
157 if count_up:
158 self.start += 1
159 else:
160 self.last += 1
161
162 def DebugString(self):
163 """Return a string that is useful in on-page debugging."""
164 return '%s - %s of %s; prev_url:%s; next_url:%s' % (
165 self.start, self.last, self.total_count, self.prev_url, self.next_url)
166
167
168class ArtifactPagination(VirtualPagination):
169 """Class to calc Prev and Next pagination links based on a results list."""
170
171 def __init__(
172 self, results, items_per_page, start, project_name, list_page_url,
173 total_count=None, limit_reached=False, skipped=0, url_params=None):
174 """Given 'num' and 'start' params, determine Prev and Next links.
175
176 Args:
177 results: a list of artifact ids that satisfy the query.
178 items_per_page: number of items to display on each page, e.g., 25.
179 start: the start index of the pagination page.
180 project_name: the name of the project we are operating in.
181 list_page_url: URL of the web application page that is displaying
182 the list of artifacts. Used to build the Prev and Next URLs.
183 total_count: specify total result count rather than the length of results
184 limit_reached: optional boolean that indicates that more results could
185 not be fetched because a limit was reached.
186 skipped: optional int number of items that were skipped and left off the
187 front of results.
188 url_params: list of (param_name, param_value) we want to keep
189 in any new urls.
190 """
191 if total_count is None:
192 total_count = skipped + len(results)
193 super(ArtifactPagination, self).__init__(
194 total_count, items_per_page, start, list_page_url=list_page_url,
195 project_name=project_name, url_params=url_params)
196
197 self.limit_reached = ezt.boolean(limit_reached)
198 # Determine which of those results should be visible on the current page.
199 range_start = self.start - 1 - skipped
200 range_end = range_start + self.num
201 assert 0 <= range_start <= range_end
202 self.visible_results = results[range_start:range_end]