| // <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; |
| } |
| } |