1<?php
2/**
3 * Copyright 2010 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18namespace Google\Service;
19
20use Google\Model;
21use Google\Http\MediaFileUpload;
22use Google\Exception as GoogleException;
23use Google\Utils\UriTemplate;
24use GuzzleHttp\Psr7\Request;
25
26/**
27 * Implements the actual methods/resources of the discovered Google API using magic function
28 * calling overloading (__call()), which on call will see if the method name (plus.activities.list)
29 * is available in this service, and if so construct an apiHttpRequest representing it.
30 *
31 */
32class Resource
33{
34  // Valid query parameters that work, but don't appear in discovery.
35  private $stackParameters = array(
36      'alt' => array('type' => 'string', 'location' => 'query'),
37      'fields' => array('type' => 'string', 'location' => 'query'),
38      'trace' => array('type' => 'string', 'location' => 'query'),
39      'userIp' => array('type' => 'string', 'location' => 'query'),
40      'quotaUser' => array('type' => 'string', 'location' => 'query'),
41      'data' => array('type' => 'string', 'location' => 'body'),
42      'mimeType' => array('type' => 'string', 'location' => 'header'),
43      'uploadType' => array('type' => 'string', 'location' => 'query'),
44      'mediaUpload' => array('type' => 'complex', 'location' => 'query'),
45      'prettyPrint' => array('type' => 'string', 'location' => 'query'),
46  );
47
48  /** @var string $rootUrl */
49  private $rootUrl;
50
51  /** @var \Google\Client $client */
52  private $client;
53
54  /** @var string $serviceName */
55  private $serviceName;
56
57  /** @var string $servicePath */
58  private $servicePath;
59
60  /** @var string $resourceName */
61  private $resourceName;
62
63  /** @var array $methods */
64  private $methods;
65
66  public function __construct($service, $serviceName, $resourceName, $resource)
67  {
68    $this->rootUrl = $service->rootUrl;
69    $this->client = $service->getClient();
70    $this->servicePath = $service->servicePath;
71    $this->serviceName = $serviceName;
72    $this->resourceName = $resourceName;
73    $this->methods = is_array($resource) && isset($resource['methods']) ?
74        $resource['methods'] :
75        array($resourceName => $resource);
76  }
77
78  /**
79   * TODO: This function needs simplifying.
80   * @param $name
81   * @param $arguments
82   * @param $expectedClass - optional, the expected class name
83   * @return mixed|$expectedClass|ResponseInterface|RequestInterface
84   * @throws \Google\Exception
85   */
86  public function call($name, $arguments, $expectedClass = null)
87  {
88    if (! isset($this->methods[$name])) {
89      $this->client->getLogger()->error(
90          'Service method unknown',
91          array(
92              'service' => $this->serviceName,
93              'resource' => $this->resourceName,
94              'method' => $name
95          )
96      );
97
98      throw new GoogleException(
99          "Unknown function: " .
100          "{$this->serviceName}->{$this->resourceName}->{$name}()"
101      );
102    }
103    $method = $this->methods[$name];
104    $parameters = $arguments[0];
105
106    // postBody is a special case since it's not defined in the discovery
107    // document as parameter, but we abuse the param entry for storing it.
108    $postBody = null;
109    if (isset($parameters['postBody'])) {
110      if ($parameters['postBody'] instanceof Model) {
111        // In the cases the post body is an existing object, we want
112        // to use the smart method to create a simple object for
113        // for JSONification.
114        $parameters['postBody'] = $parameters['postBody']->toSimpleObject();
115      } else if (is_object($parameters['postBody'])) {
116        // If the post body is another kind of object, we will try and
117        // wrangle it into a sensible format.
118        $parameters['postBody'] =
119            $this->convertToArrayAndStripNulls($parameters['postBody']);
120      }
121      $postBody = (array) $parameters['postBody'];
122      unset($parameters['postBody']);
123    }
124
125    // TODO: optParams here probably should have been
126    // handled already - this may well be redundant code.
127    if (isset($parameters['optParams'])) {
128      $optParams = $parameters['optParams'];
129      unset($parameters['optParams']);
130      $parameters = array_merge($parameters, $optParams);
131    }
132
133    if (!isset($method['parameters'])) {
134      $method['parameters'] = array();
135    }
136
137    $method['parameters'] = array_merge(
138        $this->stackParameters,
139        $method['parameters']
140    );
141
142    foreach ($parameters as $key => $val) {
143      if ($key != 'postBody' && ! isset($method['parameters'][$key])) {
144        $this->client->getLogger()->error(
145            'Service parameter unknown',
146            array(
147                'service' => $this->serviceName,
148                'resource' => $this->resourceName,
149                'method' => $name,
150                'parameter' => $key
151            )
152        );
153        throw new GoogleException("($name) unknown parameter: '$key'");
154      }
155    }
156
157    foreach ($method['parameters'] as $paramName => $paramSpec) {
158      if (isset($paramSpec['required']) &&
159          $paramSpec['required'] &&
160          ! isset($parameters[$paramName])
161      ) {
162        $this->client->getLogger()->error(
163            'Service parameter missing',
164            array(
165                'service' => $this->serviceName,
166                'resource' => $this->resourceName,
167                'method' => $name,
168                'parameter' => $paramName
169            )
170        );
171        throw new GoogleException("($name) missing required param: '$paramName'");
172      }
173      if (isset($parameters[$paramName])) {
174        $value = $parameters[$paramName];
175        $parameters[$paramName] = $paramSpec;
176        $parameters[$paramName]['value'] = $value;
177        unset($parameters[$paramName]['required']);
178      } else {
179        // Ensure we don't pass nulls.
180        unset($parameters[$paramName]);
181      }
182    }
183
184    $this->client->getLogger()->info(
185        'Service Call',
186        array(
187            'service' => $this->serviceName,
188            'resource' => $this->resourceName,
189            'method' => $name,
190            'arguments' => $parameters,
191        )
192    );
193
194    // build the service uri
195    $url = $this->createRequestUri(
196        $method['path'],
197        $parameters
198    );
199
200    // NOTE: because we're creating the request by hand,
201    // and because the service has a rootUrl property
202    // the "base_uri" of the Http Client is not accounted for
203    $request = new Request(
204        $method['httpMethod'],
205        $url,
206        ['content-type' => 'application/json'],
207        $postBody ? json_encode($postBody) : ''
208    );
209
210    // support uploads
211    if (isset($parameters['data'])) {
212      $mimeType = isset($parameters['mimeType'])
213        ? $parameters['mimeType']['value']
214        : 'application/octet-stream';
215      $data = $parameters['data']['value'];
216      $upload = new MediaFileUpload($this->client, $request, $mimeType, $data);
217
218      // pull down the modified request
219      $request = $upload->getRequest();
220    }
221
222    // if this is a media type, we will return the raw response
223    // rather than using an expected class
224    if (isset($parameters['alt']) && $parameters['alt']['value'] == 'media') {
225      $expectedClass = null;
226    }
227
228    // if the client is marked for deferring, rather than
229    // execute the request, return the response
230    if ($this->client->shouldDefer()) {
231      // @TODO find a better way to do this
232      $request = $request
233        ->withHeader('X-Php-Expected-Class', $expectedClass);
234
235      return $request;
236    }
237
238    return $this->client->execute($request, $expectedClass);
239  }
240
241  protected function convertToArrayAndStripNulls($o)
242  {
243    $o = (array) $o;
244    foreach ($o as $k => $v) {
245      if ($v === null) {
246        unset($o[$k]);
247      } elseif (is_object($v) || is_array($v)) {
248        $o[$k] = $this->convertToArrayAndStripNulls($o[$k]);
249      }
250    }
251    return $o;
252  }
253
254  /**
255   * Parse/expand request parameters and create a fully qualified
256   * request uri.
257   * @static
258   * @param string $restPath
259   * @param array $params
260   * @return string $requestUrl
261   */
262  public function createRequestUri($restPath, $params)
263  {
264    // Override the default servicePath address if the $restPath use a /
265    if ('/' == substr($restPath, 0, 1)) {
266      $requestUrl = substr($restPath, 1);
267    } else {
268      $requestUrl = $this->servicePath . $restPath;
269    }
270
271    // code for leading slash
272    if ($this->rootUrl) {
273      if ('/' !== substr($this->rootUrl, -1) && '/' !== substr($requestUrl, 0, 1)) {
274        $requestUrl = '/' . $requestUrl;
275      }
276      $requestUrl = $this->rootUrl . $requestUrl;
277    }
278    $uriTemplateVars = array();
279    $queryVars = array();
280    foreach ($params as $paramName => $paramSpec) {
281      if ($paramSpec['type'] == 'boolean') {
282        $paramSpec['value'] = $paramSpec['value'] ? 'true' : 'false';
283      }
284      if ($paramSpec['location'] == 'path') {
285        $uriTemplateVars[$paramName] = $paramSpec['value'];
286      } else if ($paramSpec['location'] == 'query') {
287        if (is_array($paramSpec['value'])) {
288          foreach ($paramSpec['value'] as $value) {
289            $queryVars[] = $paramName . '=' . rawurlencode(rawurldecode($value));
290          }
291        } else {
292          $queryVars[] = $paramName . '=' . rawurlencode(rawurldecode($paramSpec['value']));
293        }
294      }
295    }
296
297    if (count($uriTemplateVars)) {
298      $uriTemplateParser = new UriTemplate();
299      $requestUrl = $uriTemplateParser->parse($requestUrl, $uriTemplateVars);
300    }
301
302    if (count($queryVars)) {
303      $requestUrl .= '?' . implode('&', $queryVars);
304    }
305
306    return $requestUrl;
307  }
308}
309