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.
33 public function __construct(GadgetContext
$context, $token) {
34 $this->context
= $context;
35 $this->token
= $token;
39 * Returns the processed gadget spec
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);
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;
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;
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();
121 $gadget->substitutions
->addSubstitution('MODULE', "ID", $this->token
->getModuleId());
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'];
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'];
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']);
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();