Path: blob/trunk/dotnet/src/webdriver/Firefox/FirefoxExtension.cs
2885 views
// <copyright file="FirefoxExtension.cs" company="Selenium Committers"> // Licensed to the Software Freedom Conservancy (SFC) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The SFC licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // </copyright> using OpenQA.Selenium.Internal; using System; using System.Globalization; using System.IO; using System.IO.Compression; using System.Text.Json.Nodes; using System.Xml; namespace OpenQA.Selenium.Firefox; /// <summary> /// Provides the ability to install extensions into a <see cref="FirefoxProfile"/>. /// </summary> public class FirefoxExtension { private const string EmNamespaceUri = "http://www.mozilla.org/2004/em-rdf#"; private const string RdfManifestFileName = "install.rdf"; private const string JsonManifestFileName = "manifest.json"; private readonly string extensionFileName; private readonly string extensionResourceId; /// <summary> /// Initializes a new instance of the <see cref="FirefoxExtension"/> class. /// </summary> /// <param name="fileName">The name of the file containing the Firefox extension.</param> /// <remarks>WebDriver attempts to resolve the <paramref name="fileName"/> parameter /// by looking first for the specified file in the directory of the calling assembly, /// then using the full path to the file, if a full path is provided.</remarks> /// <exception cref="ArgumentNullException">If <paramref name="fileName"/> is <see langword="null"/>.</exception> public FirefoxExtension(string fileName) : this(fileName, string.Empty) { } /// <summary> /// Initializes a new instance of the <see cref="FirefoxExtension"/> class. /// </summary> /// <param name="fileName">The name of the file containing the Firefox extension.</param> /// <param name="resourceId">The ID of the resource within the assembly containing the extension /// if the file is not present in the file system.</param> /// <remarks>WebDriver attempts to resolve the <paramref name="fileName"/> parameter /// by looking first for the specified file in the directory of the calling assembly, /// then using the full path to the file, if a full path is provided. If the file is /// not found in the file system, WebDriver attempts to locate a resource in the /// executing assembly with the name specified by the <paramref name="resourceId"/> /// parameter.</remarks> /// <exception cref="ArgumentNullException">If <paramref name="fileName"/> or <paramref name="resourceId"/> are <see langword="null"/>.</exception> internal FirefoxExtension(string fileName, string resourceId) { this.extensionFileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); this.extensionResourceId = resourceId ?? throw new ArgumentNullException(nameof(resourceId)); } /// <summary> /// Installs the extension into a profile directory. /// </summary> /// <param name="profileDirectory">The Firefox profile directory into which to install the extension.</param> /// <exception cref="ArgumentNullException">If <paramref name="profileDirectory"/> is <see langword="null"/>.</exception> public void Install(string profileDirectory) { DirectoryInfo info = new DirectoryInfo(profileDirectory); string stagingDirectoryName = Path.Combine(Path.GetTempPath(), info.Name + ".staging"); string tempFileName = Path.Combine(stagingDirectoryName, Path.GetFileName(this.extensionFileName)); if (Directory.Exists(tempFileName)) { Directory.Delete(tempFileName, true); } // First, expand the .xpi archive into a temporary location. Directory.CreateDirectory(tempFileName); Stream zipFileStream = ResourceUtilities.GetResourceStream(this.extensionFileName, this.extensionResourceId); using (ZipArchive extensionZipArchive = new ZipArchive(zipFileStream, ZipArchiveMode.Read)) { extensionZipArchive.ExtractToDirectory(tempFileName); } // Then, copy the contents of the temporary location into the // proper location in the Firefox profile directory. string id = GetExtensionId(tempFileName); string extensionDirectory = Path.Combine(Path.Combine(profileDirectory, "extensions"), id); if (Directory.Exists(extensionDirectory)) { Directory.Delete(extensionDirectory, true); } Directory.CreateDirectory(extensionDirectory); FileUtilities.CopyDirectory(tempFileName, extensionDirectory); // By deleting the staging directory, we also delete the temporarily // expanded extension, which we copied into the profile. FileUtilities.DeleteDirectory(stagingDirectoryName); } private static string GetExtensionId(string root) { // Checks if manifest.json or install.rdf file exists and extracts // the addon/extension id from the file accordingly string manifestJsonPath = Path.Combine(root, JsonManifestFileName); string installRdfPath = Path.Combine(root, RdfManifestFileName); if (File.Exists(installRdfPath)) { return ReadIdFromInstallRdf(root); } if (File.Exists(manifestJsonPath)) { return ReadIdFromManifestJson(root); } throw new WebDriverException("Extension should contain either install.rdf or manifest.json metadata file"); } private static string ReadIdFromInstallRdf(string root) { string id; string installRdf = Path.Combine(root, "install.rdf"); try { XmlDocument rdfXmlDocument = new XmlDocument(); rdfXmlDocument.Load(installRdf); XmlNamespaceManager rdfNamespaceManager = new XmlNamespaceManager(rdfXmlDocument.NameTable); rdfNamespaceManager.AddNamespace("em", EmNamespaceUri); rdfNamespaceManager.AddNamespace("RDF", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); XmlNode? node = rdfXmlDocument.SelectSingleNode("//em:id", rdfNamespaceManager); if (node == null) { XmlNode? descriptionNode = rdfXmlDocument.SelectSingleNode("//RDF:Description", rdfNamespaceManager); XmlAttribute? attribute = descriptionNode?.Attributes?["id", EmNamespaceUri]; if (attribute == null) { throw new WebDriverException("Cannot locate node containing extension id: " + installRdf); } id = attribute.Value; } else { id = node.InnerText; } if (string.IsNullOrEmpty(id)) { throw new FileNotFoundException("Cannot install extension with ID: " + id); } } catch (Exception e) { throw new WebDriverException("Error installing extension", e); } return id; } private static string ReadIdFromManifestJson(string root) { string id = string.Empty; string manifestJsonPath = Path.Combine(root, JsonManifestFileName); var manifestObject = JsonNode.Parse(File.ReadAllText(manifestJsonPath)); if (manifestObject!["applications"]?["gecko"]?["id"] is { } idNode) { id = idNode.ToString().Trim(); } if (string.IsNullOrEmpty(id)) { string addInName = manifestObject["name"]!.ToString().Replace(" ", ""); string addInVersion = manifestObject["version"]!.ToString(); id = string.Format(CultureInfo.InvariantCulture, "{0}@{1}", addInName, addInVersion); } return id; } }