Rolling back the patch from SHINDIG-966, was breaking makeRequest
[shindig.git] / php / src / gadgets / GadgetFactory.php
blob8e9d8ba9998af18d7ca28504ef3565ddacef8b41
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 private $context;
28 private $token;
30 public function __construct(GadgetContext $context, $token) {
31 $this->context = $context;
32 $this->token = $token;
35 /**
36 * Returns the processed gadget spec
38 * @return GadgetSpec
40 public function createGadget() {
41 $gadgetUrl = $this->context->getUrl();
42 if ($this->context->getBlacklist() != null && $this->context->getBlacklist()->isBlacklisted($gadgetUrl)) {
43 throw new GadgetException("The Gadget ($gadgetUrl) is blacklisted and can not be rendered");
45 // Fetch the gadget's content and create a GadgetSpec
46 $gadgetContent = $this->fetchGadget($gadgetUrl);
47 $gadgetSpecParser = new GadgetSpecParser();
48 $gadgetSpec = $gadgetSpecParser->parse($gadgetContent);
49 $gadget = new Gadget($gadgetSpec, $this->context);
51 // Process the gadget: fetching remote resources, processing & applying the correct translations, user prefs and feature resolving
52 $this->fetchResources($gadget);
53 $this->mergeLocales($gadget);
54 $this->parseUserPrefs($gadget);
55 $this->addSubstitutions($gadget);
56 $this->applySubstitutions($gadget);
57 $this->parseFeatures($gadget);
58 return $gadget;
61 /**
62 * Resolves the Required and Optional features and their dependencies into a real feature list using
63 * the GadgetFeatureRegistry, which can be used to construct the javascript for the gadget
65 * @param Gadget $gadget
67 private function parseFeatures(Gadget &$gadget) {
68 $found = $missing = array();
69 if (!$this->context->getRegistry()->resolveFeatures(array_merge($gadget->gadgetSpec->requiredFeatures, $gadget->gadgetSpec->optionalFeatures), $found, $missing)) {
70 $requiredMissing = false;
71 foreach ($missing as $featureName) {
72 if (in_array($featureName, $gadget->gadgetSpec->requiredFeatures)) {
73 $requiredMissing = true;
74 break;
77 if ($requiredMissing) {
78 throw new GadgetException("Unknown features: ".implode(',', $missing));
81 unset($gadget->gadgetSpec->optionalFeatures);
82 unset($gadget->gadgetSpec->requiredFeatures);
83 $gadget->features = $found;
86 /**
87 * Applies the substitutions to the complex types (preloads, user prefs, etc). Simple
88 * types (author, title, etc) are translated on the fly in the gadget's getFoo() functions
90 private function applySubstitutions(Gadget &$gadget) {
91 // Apply the substitutions to the UserPrefs
92 foreach ($gadget->gadgetSpec->userPrefs as $key => $pref) {
93 $gadget->gadgetSpec->userPrefs[$key]['name'] = $gadget->substitutions->substitute($pref['name']);
94 $gadget->gadgetSpec->userPrefs[$key]['displayName'] = $gadget->substitutions->substitute($pref['displayName']);
95 $gadget->gadgetSpec->userPrefs[$key]['required'] = $gadget->substitutions->substitute($pref['required']);
96 $gadget->gadgetSpec->userPrefs[$key]['datatype'] = $gadget->substitutions->substitute($pref['datatype']);
97 $gadget->gadgetSpec->userPrefs[$key]['defaultValue'] = $gadget->substitutions->substitute($pref['defaultValue']);
98 $gadget->gadgetSpec->userPrefs[$key]['value'] = $gadget->substitutions->substitute($pref['value']);
99 if (isset($pref['enumValues'])) {
100 foreach ($pref['enumValues'] as $enumKey => $enumVal) {
101 $gadget->gadgetSpec->userPrefs[$key]['enumValues'][$enumKey]['value'] = $gadget->substitutions->substitute($enumVal['value']);
102 $gadget->gadgetSpec->userPrefs[$key]['enumValues'][$enumKey]['displayValue'] = $gadget->substitutions->substitute($enumVal['displayValue']);
106 // Apply substitutions to the preloads
107 foreach ($gadget->gadgetSpec->preloads as $url => $preload) {
108 $gadget->gadgetSpec->preloads[$url]['body'] = $gadget->substitutions->substitute($preload['body']);
113 * Seeds the substitutions class with the user prefs, messages, bidi and module id
115 private function addSubstitutions(Gadget &$gadget) {
116 $gadget->substitutions = new Substitutions();
117 if ($this->token) {
118 $gadget->substitutions->addSubstitution('MODULE', "ID", $this->token->getModuleId());
120 if ($gadget->gadgetSpec->locales) {
121 $gadget->substitutions->addSubstitutions('MSG', $gadget->gadgetSpec->locales);
123 $gadget->substitutions->addSubstitution('BIDI', "START_EDGE", $gadget->rightToLeft ? "right" : "left");
124 $gadget->substitutions->addSubstitution('BIDI', "END_EDGE", $gadget->rightToLeft ? "left" : "right");
125 $gadget->substitutions->addSubstitution('BIDI', "DIR", $gadget->rightToLeft ? "rtl" : "ltr");
126 $gadget->substitutions->addSubstitution('BIDI', "REVERSE_DIR", $gadget->rightToLeft ? "ltr" : "rtl");
127 foreach ($gadget->gadgetSpec->userPrefs as $pref) {
128 $gadget->substitutions->addSubstitution('UP', $gadget->substitutions->substitute($pref['name']), $gadget->substitutions->substitute($pref['value']));
132 * Process the UserPrefs values based on the current context
134 * @param Gadget $gadget
136 private function parseUserPrefs(Gadget &$gadget) {
137 foreach ($gadget->gadgetSpec->userPrefs as $key => $pref) {
138 $queryKey = 'up_'.$pref['name'];
139 $gadget->gadgetSpec->userPrefs[$key]['value'] = isset($_GET[$queryKey]) ? trim(urldecode($_GET[$queryKey])) : $pref['defaultValue'];
144 * Merges all matching Message bundles, with a full match (lang and country) having the
145 * highest priority and all/all having the lowest.
147 * This distills the locales array's back to one array of translations, which is then exposed
148 * through the $gadget->substitutions class
150 * @param Gadget $gadget
152 private function mergeLocales(Gadget $gadget) {
153 if (count($gadget->gadgetSpec->locales)) {
154 $contextLocale = $this->context->getLocale();
155 $locales = $gadget->gadgetSpec->locales;
156 $gadget->rightToLeft = false;
157 $full = $partial = $all = null;
158 foreach ($locales as $locale) {
159 if ($locale['lang'] == $contextLocale['lang'] && $locale['country'] == $contextLocale['country']) {
160 $full = $locale['messageBundle'];
161 $gadget->rightToLeft = $locale['languageDirection'] == 'rtl';
162 } elseif ($locale['lang'] == $contextLocale['lang'] && $locale['country'] == 'all') {
163 $partial = $locale['messageBundle'];
164 } elseif ($locale['country'] == 'all' && $locale['lang'] == 'all') {
165 $all = $locale['messageBundle'];
168 $gadget->gadgetSpec->locales = array();
169 // array_merge overwrites duplicate keys from param 2 over param 1, so $full takes precedence over partial, and it over all
170 if ($full) $gadget->gadgetSpec->locales = array_merge($full, $gadget->gadgetSpec->locales);
171 if ($partial) $gadget->gadgetSpec->locales = array_merge($partial, $gadget->gadgetSpec->locales);
172 if ($all) $gadget->gadgetSpec->locales = array_merge($all, $gadget->gadgetSpec->locales);
177 * Fetches all remote resources simultaniously using a multiFetchRequest to optimize rendering time.
179 * The preloads will be json_encoded to their gadget document injection format, and the locales will
180 * be reduced to only the GadgetContext->getLocale matching entries.
182 * @param Gadget $gadget
183 * @param GadgetContext $context
185 private function fetchResources(Gadget &$gadget) {
186 $contextLocale = $this->context->getLocale();
187 $unsignedRequests = $unsignedContexts = $signedRequests = array();
188 foreach ($gadget->getLocales() as $key => $locale) {
189 // Only fetch the locales that match the current context's language and country
190 if (($locale['country'] == 'all' && $locale['lang'] == 'all') || ($locale['lang'] == $contextLocale['lang'] && $locale['country'] == 'all') || ($locale['lang'] == $contextLocale['lang'] && $locale['country'] == $contextLocale['country'])) {
191 if (!empty($locale['messages'])) {
192 // locale matches the current context, add it to the requests queue
193 $unsignedRequests[] = $locale['messages'];
195 } else {
196 // remove any locales that are not applicable to this context
197 unset($gadget->gadgetSpec->locales[$key]);
200 // Add preloads to the request queue
201 foreach ($gadget->getPreloads() as $preload) {
202 if (!empty($preload['href'])) {
203 if (!empty($preload['authz']) && $preload['authz'] == 'SIGNED') {
204 if ($this->token == '') {
205 throw new GadgetException("Signed preloading requested, but no valid security token set");
207 $signedRequests[] = $preload['href'];
208 } else {
209 $unsignedRequests[] = $preload['href'];
213 // Perform the non-signed requests
214 foreach ($unsignedRequests as $key => $requestUrl) {
215 $request = new RemoteContentRequest($requestUrl);
216 $request->createRemoteContentRequestWithUri($requestUrl);
217 $unsignedRequests[$key] = $request;
218 $unsignedContexts[$key] = $this->context;
220 $responses = array();
221 if (count($unsignedRequests)) {
222 $brc = new BasicRemoteContent();
223 $resps = $brc->multiFetch($unsignedRequests, $unsignedContexts);
224 foreach ($resps as $response) {
225 $responses[$response->getUrl()] = array(
226 'body' => $response->getResponseContent(),
227 'rc' => $response->getHttpCode());
230 // Perform the signed requests
231 foreach ($signedRequests as $key => $requestUrl) {
232 $request = new RemoteContentRequest($requestUrl);
233 $request->createRemoteContentRequestWithUri($requestUrl);
234 $signedRequests[$key] = $request;
235 $signingFetcherFactory = new SigningFetcherFactory(Config::get("private_key_file"));
236 $fetcher = $signingFetcherFactory->getSigningFetcher(new BasicRemoteContentFetcher(), $this->token);
237 $req = $fetcher->signRequest($requestUrl, 'GET');
238 $req->setNotSignedUri($requestUrl);
239 $signedRequests[] = $req;
241 if (count($signedRequests)) {
242 $fetcher = $signingFetcherFactory->getSigningFetcher(new BasicRemoteContentFetcher(), $this->token);
243 $resps = $fetcher->multiFetchRequest($signedRequests);
244 foreach ($resps as $response) {
245 $responses[$response->getNotSignedUrl()] = array(
246 'body' => $response->getResponseContent(),
247 'rc' => $response->getHttpCode());
250 // assign the results to the gadget locales and preloads (using the url as the key)
251 foreach ($gadget->gadgetSpec->locales as $key => $locale) {
252 if (!empty($locale['messages']) && isset($responses[$locale['messages']]) && $responses[$locale['messages']]['rc'] == 200) {
253 $gadget->gadgetSpec->locales[$key]['messageBundle'] = $this->parseMessageBundle($responses[$locale['messages']]['body']);
256 $preloads = array();
257 foreach ($gadget->gadgetSpec->preloads as $key => $preload) {
258 if (!empty($preload['href']) && isset($responses[$preload['href']]) && $responses[$preload['href']]['rc'] == 200) {
259 $preloads[$preload['href']] = $responses[$preload['href']];
262 $gadget->gadgetSpec->preloads = $preloads;
266 * Parses the (remote / fetched) message bundle xml
268 * @param string $messageBundleData
269 * @return array (MessageBundle)
271 private function parseMessageBundle($messageBundleData) {
272 libxml_use_internal_errors(true);
273 $doc = new DOMDocument();
274 if (! $doc->loadXML($messageBundleData, LIBXML_NOCDATA)) {
275 $errors = libxml_get_errors();
276 $errorStr = '';
277 foreach ($errors as $error) {
278 $errorStr .= $error . " \n";
280 libxml_clear_errors();
281 throw new GadgetSpecException("Error parsing gadget xml:\n$errorStr");
283 $messageBundle = array();
284 if (($messageBundleNode = $doc->getElementsByTagName('messagebundle')) != null && $messageBundleNode->length > 0) {
285 $messageBundleNode = $messageBundleNode->item(0);
286 $messages = $messageBundleNode->getElementsByTagName('msg');
287 foreach ($messages as $msg) {
288 $messageBundle[$msg->getAttribute('name')] = trim($msg->nodeValue);
291 return $messageBundle;
295 * Fetches the gadget xml for the requested URL using the http fetcher
297 * @param unknown_type $gadgetUrl
298 * @return string gadget's xml content
300 protected function fetchGadget($gadgetUrl) {
301 $request = new RemoteContentRequest($gadgetUrl);
302 $xml = $this->context->getHttpFetcher()->fetch($request, $this->context);
303 if ($xml->getHttpCode() != '200') {
304 throw new GadgetException("Failed to retrieve gadget content (recieved http code " . $xml->getHttpCode() . ")");
306 return $xml->getResponseContent();