SHINDIG-1056 by lipeng, BasicRemoteContentTest doesn't depend on static private key...
[shindig.git] / php / src / gadgets / GadgetFactory.php
blobf694cc7d1a07e9761dce004e71f50be9af39d0e0
1 <?php
2 /**
3 * Licensed to the Apache Software Foundation (ASF) under one
4 * or more contributor license agreements. See the NOTICE file
5 * distributed with this work for additional information
6 * regarding copyright ownership. The ASF licenses this file
7 * to you under the Apache License, Version 2.0 (the
8 * "License"); you may not use this file except in compliance
9 * with the License. You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing,
14 * software distributed under the License is distributed on an
15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 * KIND, either express or implied. See the License for the
17 * specific language governing permissions and limitations
18 * under the License.
21 /**
22 * The Gadget Factory builds a gadget based on the current context and token and returns a fully processed
23 * gadget ready to be rendered.
26 class GadgetFactory {
27 /**
28 * @var GadgetContext
30 private $context;
31 private $token;
33 public function __construct(GadgetContext $context, $token) {
34 $this->context = $context;
35 $this->token = $token;
38 /**
39 * Returns the processed gadget spec
41 * @return GadgetSpec
43 public function createGadget() {
44 $gadgetUrl = $this->context->getUrl();
45 if ($this->context->getBlacklist() != null && $this->context->getBlacklist()->isBlacklisted($gadgetUrl)) {
46 throw new GadgetException("The Gadget ($gadgetUrl) is blacklisted and can not be rendered");
48 // Fetch the gadget's content and create a GadgetSpec
49 $gadgetContent = $this->fetchGadget($gadgetUrl);
50 $gadgetSpecParser = new GadgetSpecParser();
51 $gadgetSpec = $gadgetSpecParser->parse($gadgetContent);
52 $gadget = new Gadget($gadgetSpec, $this->context);
54 // Process the gadget: fetching remote resources, processing & applying the correct translations, user prefs and feature resolving
55 $this->fetchResources($gadget);
56 $this->mergeLocales($gadget);
57 $this->parseUserPrefs($gadget);
58 $this->addSubstitutions($gadget);
59 $this->applySubstitutions($gadget);
60 $this->parseFeatures($gadget);
61 return $gadget;
64 /**
65 * Resolves the Required and Optional features and their dependencies into a real feature list using
66 * the GadgetFeatureRegistry, which can be used to construct the javascript for the gadget
68 * @param Gadget $gadget
70 private function parseFeatures(Gadget &$gadget) {
71 $found = $missing = array();
72 if (!$this->context->getRegistry()->resolveFeatures(array_merge($gadget->gadgetSpec->requiredFeatures, $gadget->gadgetSpec->optionalFeatures), $found, $missing)) {
73 $requiredMissing = false;
74 foreach ($missing as $featureName) {
75 if (in_array($featureName, $gadget->gadgetSpec->requiredFeatures)) {
76 $requiredMissing = true;
77 break;
80 if ($requiredMissing) {
81 throw new GadgetException("Unknown features: ".implode(',', $missing));
84 unset($gadget->gadgetSpec->optionalFeatures);
85 unset($gadget->gadgetSpec->requiredFeatures);
86 $gadget->features = $found;
89 /**
90 * Applies the substitutions to the complex types (preloads, user prefs, etc). Simple
91 * types (author, title, etc) are translated on the fly in the gadget's getFoo() functions
93 private function applySubstitutions(Gadget &$gadget) {
94 // Apply the substitutions to the UserPrefs
95 foreach ($gadget->gadgetSpec->userPrefs as $key => $pref) {
96 $gadget->gadgetSpec->userPrefs[$key]['name'] = $gadget->substitutions->substitute($pref['name']);
97 $gadget->gadgetSpec->userPrefs[$key]['displayName'] = $gadget->substitutions->substitute($pref['displayName']);
98 $gadget->gadgetSpec->userPrefs[$key]['required'] = $gadget->substitutions->substitute($pref['required']);
99 $gadget->gadgetSpec->userPrefs[$key]['datatype'] = $gadget->substitutions->substitute($pref['datatype']);
100 $gadget->gadgetSpec->userPrefs[$key]['defaultValue'] = $gadget->substitutions->substitute($pref['defaultValue']);
101 $gadget->gadgetSpec->userPrefs[$key]['value'] = $gadget->substitutions->substitute($pref['value']);
102 if (isset($pref['enumValues'])) {
103 foreach ($pref['enumValues'] as $enumKey => $enumVal) {
104 $gadget->gadgetSpec->userPrefs[$key]['enumValues'][$enumKey]['value'] = $gadget->substitutions->substitute($enumVal['value']);
105 $gadget->gadgetSpec->userPrefs[$key]['enumValues'][$enumKey]['displayValue'] = $gadget->substitutions->substitute($enumVal['displayValue']);
109 // Apply substitutions to the preloads
110 foreach ($gadget->gadgetSpec->preloads as $url => $preload) {
111 $gadget->gadgetSpec->preloads[$url]['body'] = $gadget->substitutions->substitute($preload['body']);
116 * Seeds the substitutions class with the user prefs, messages, bidi and module id
118 private function addSubstitutions(Gadget &$gadget) {
119 $gadget->substitutions = new Substitutions();
120 if ($this->token) {
121 $gadget->substitutions->addSubstitution('MODULE', "ID", $this->token->getModuleId());
122 } else {
123 $gadget->substitutions->addSubstitution('MODULE', "ID", 0);
125 if ($gadget->gadgetSpec->locales) {
126 $gadget->substitutions->addSubstitutions('MSG', $gadget->gadgetSpec->locales);
128 $gadget->substitutions->addSubstitution('BIDI', "START_EDGE", $gadget->rightToLeft ? "right" : "left");
129 $gadget->substitutions->addSubstitution('BIDI', "END_EDGE", $gadget->rightToLeft ? "left" : "right");
130 $gadget->substitutions->addSubstitution('BIDI', "DIR", $gadget->rightToLeft ? "rtl" : "ltr");
131 $gadget->substitutions->addSubstitution('BIDI', "REVERSE_DIR", $gadget->rightToLeft ? "ltr" : "rtl");
132 foreach ($gadget->gadgetSpec->userPrefs as $pref) {
133 $gadget->substitutions->addSubstitution('UP', $gadget->substitutions->substitute($pref['name']), $gadget->substitutions->substitute($pref['value']));
137 * Process the UserPrefs values based on the current context
139 * @param Gadget $gadget
141 private function parseUserPrefs(Gadget &$gadget) {
142 foreach ($gadget->gadgetSpec->userPrefs as $key => $pref) {
143 $queryKey = 'up_'.$pref['name'];
144 $gadget->gadgetSpec->userPrefs[$key]['value'] = isset($_GET[$queryKey]) ? trim(urldecode($_GET[$queryKey])) : $pref['defaultValue'];
149 * Merges all matching Message bundles, with a full match (lang and country) having the
150 * highest priority and all/all having the lowest.
152 * This distills the locales array's back to one array of translations, which is then exposed
153 * through the $gadget->substitutions class
155 * @param Gadget $gadget
157 private function mergeLocales(Gadget $gadget) {
158 if (count($gadget->gadgetSpec->locales)) {
159 $contextLocale = $this->context->getLocale();
160 $locales = $gadget->gadgetSpec->locales;
161 $gadget->rightToLeft = false;
162 $full = $partial = $all = null;
163 foreach ($locales as $locale) {
164 if ($locale['lang'] == $contextLocale['lang'] && $locale['country'] == $contextLocale['country']) {
165 $full = $locale['messageBundle'];
166 $gadget->rightToLeft = $locale['languageDirection'] == 'rtl';
167 } elseif ($locale['lang'] == $contextLocale['lang'] && $locale['country'] == 'all') {
168 $partial = $locale['messageBundle'];
169 } elseif ($locale['country'] == 'all' && $locale['lang'] == 'all') {
170 $all = $locale['messageBundle'];
173 $gadget->gadgetSpec->locales = array();
174 // array_merge overwrites duplicate keys from param 2 over param 1, so $full takes precedence over partial, and it over all
175 if ($full) $gadget->gadgetSpec->locales = array_merge($full, $gadget->gadgetSpec->locales);
176 if ($partial) $gadget->gadgetSpec->locales = array_merge($partial, $gadget->gadgetSpec->locales);
177 if ($all) $gadget->gadgetSpec->locales = array_merge($all, $gadget->gadgetSpec->locales);
182 * Fetches all remote resources simultaniously using a multiFetchRequest to optimize rendering time.
184 * The preloads will be json_encoded to their gadget document injection format, and the locales will
185 * be reduced to only the GadgetContext->getLocale matching entries.
187 * @param Gadget $gadget
188 * @param GadgetContext $context
190 private function fetchResources(Gadget &$gadget) {
191 $contextLocale = $this->context->getLocale();
192 $unsignedRequests = $signedRequests = array();
193 foreach ($gadget->getLocales() as $key => $locale) {
194 // Only fetch the locales that match the current context's language and country
195 if (($locale['country'] == 'all' && $locale['lang'] == 'all') || ($locale['lang'] == $contextLocale['lang'] && $locale['country'] == 'all') || ($locale['lang'] == $contextLocale['lang'] && $locale['country'] == $contextLocale['country'])) {
196 if (!empty($locale['messages'])) {
197 // locale matches the current context, add it to the requests queue
198 $unsignedRequests[] = $locale['messages'];
200 } else {
201 // remove any locales that are not applicable to this context
202 unset($gadget->gadgetSpec->locales[$key]);
205 // Add preloads to the request queue
206 foreach ($gadget->getPreloads() as $preload) {
207 if (!empty($preload['href'])) {
208 if (!empty($preload['authz']) && $preload['authz'] == 'SIGNED') {
209 if ($this->token == '') {
210 throw new GadgetException("Signed preloading requested, but no valid security token set");
212 $signedRequests[] = $preload['href'];
213 } else {
214 $unsignedRequests[] = $preload['href'];
218 // Perform the non-signed requests
219 foreach ($unsignedRequests as $key => $requestUrl) {
220 $request = new RemoteContentRequest($requestUrl);
221 $request->createRemoteContentRequestWithUri($requestUrl);
222 $request->getOptions()->ignoreCache = $this->context->getIgnoreCache();
223 $unsignedRequests[$key] = $request;
225 $responses = array();
226 if (count($unsignedRequests)) {
227 $brc = new BasicRemoteContent();
228 $resps = $brc->multiFetch($unsignedRequests);
229 foreach ($resps as $response) {
230 $responses[$response->getUrl()] = array(
231 'body' => $response->getResponseContent(),
232 'rc' => $response->getHttpCode());
235 // Perform the signed requests
236 foreach ($signedRequests as $key => $requestUrl) {
237 $request = new RemoteContentRequest($requestUrl);
238 $request->setAuthType(RemoteContentRequest::$AUTH_SIGNED);
239 $request->setNotSignedUri($requestUrl);
240 $request->getOptions()->ignoreCache = $this->context->getIgnoreCache();
241 $signedRequests[$key] = $request;
243 if (count($signedRequests)) {
244 $remoteContent = new BasicRemoteContent(new BasicRemoteContentFetcher(), $signingFetcherFactory);
245 $resps = $remoteContent->multiFetch($signedRequests);
246 foreach ($resps as $response) {
247 $responses[$response->getNotSignedUrl()] = array(
248 'body' => $response->getResponseContent(),
249 'rc' => $response->getHttpCode());
252 // assign the results to the gadget locales and preloads (using the url as the key)
253 foreach ($gadget->gadgetSpec->locales as $key => $locale) {
254 if (!empty($locale['messages']) && isset($responses[$locale['messages']]) && $responses[$locale['messages']]['rc'] == 200) {
255 $gadget->gadgetSpec->locales[$key]['messageBundle'] = $this->parseMessageBundle($responses[$locale['messages']]['body']);
258 $preloads = array();
259 foreach ($gadget->gadgetSpec->preloads as $key => $preload) {
260 if (!empty($preload['href']) && isset($responses[$preload['href']]) && $responses[$preload['href']]['rc'] == 200) {
261 $preloads[$preload['href']] = $responses[$preload['href']];
264 $gadget->gadgetSpec->preloads = $preloads;
268 * Parses the (remote / fetched) message bundle xml
270 * @param string $messageBundleData
271 * @return array (MessageBundle)
273 private function parseMessageBundle($messageBundleData) {
274 libxml_use_internal_errors(true);
275 $doc = new DOMDocument();
276 if (! $doc->loadXML($messageBundleData, LIBXML_NOCDATA)) {
277 throw new GadgetSpecException("Error parsing gadget xml:\n".XmlError::getErrors($messageBundleData));
279 $messageBundle = array();
280 if (($messageBundleNode = $doc->getElementsByTagName('messagebundle')) != null && $messageBundleNode->length > 0) {
281 $messageBundleNode = $messageBundleNode->item(0);
282 $messages = $messageBundleNode->getElementsByTagName('msg');
283 foreach ($messages as $msg) {
284 $messageBundle[$msg->getAttribute('name')] = trim($msg->nodeValue);
287 return $messageBundle;
291 * Fetches the gadget xml for the requested URL using the http fetcher
293 * @param unknown_type $gadgetUrl
294 * @return string gadget's xml content
296 protected function fetchGadget($gadgetUrl) {
297 $request = new RemoteContentRequest($gadgetUrl);
298 $request->setToken($this->token);
299 $request->getOptions()->ignoreCache = $this->context->getIgnoreCache();
300 $xml = $this->context->getHttpFetcher()->fetch($request);
301 if ($xml->getHttpCode() != '200') {
302 throw new GadgetException("Failed to retrieve gadget content (recieved http code " . $xml->getHttpCode() . ")");
304 return $xml->getResponseContent();