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