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
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.
30 public function __construct(GadgetContext
$context, $token) {
31 $this->context
= $context;
32 $this->token
= $token;
36 * Returns the processed gadget spec
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);
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;
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;
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();
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'];
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'];
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']);
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();
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();